1.

Djangoビューの基本と子ページ案内|FBV・CBV・入力検証・権限

▶

Djangoのビューは、リクエストから必要な処理を行い応答を返す入口です。ファイル分割だけでなく、入力と操作権限をどこで確認するかを分けて読みます。

Viewに関する項目を子ページから選択してください。

HTTPとCSRF、ログインと操作権限、対象データ範囲、フォーム検証を分けて更新する図

この記事の要点
  • Django の View はリクエストに応答する処理。views.pyは慣習で、複数ファイルにも分けられる
  • 書き方は 2 種類: Function-based View (FBV) と Class-based View (CBV)
  • レスポンス: HttpResponse / render(request, "app/index.html", context) / redirect("name") / JsonResponse
  • ジェネリック CBV: ListView / DetailView / CreateView / UpdateView / DeleteView は共通処理を再利用する。認証・権限・フォーム・テンプレートは別途設計する
  • URL マッピングは urls.py の path()。デコレータ @login_required / @require_http_methods で認証・メソッド制限

View とは

Django MVT (Model-View-Template) パターンの「View」は、Web リクエストを受け取り、必要な処理を実行してレスポンスを返す Python 関数 / クラスです。他のMVCでControllerと呼ばれる処理に似る面もありますが、名称だけの1対1対応ではありません。

views.pyは慣習的な配置です。URLconfからimportできる呼出し先であれば別モジュールやviews/パッケージにも分けられます。DjangoのMVTと他のMVCを名前だけで1対1対応とせず、URL選択・middleware・ビュー・テンプレートを区別します。

目的に合わせて子ページを選ぶ

Viewに関する項目を子ページから選択してください。このページは役割と境界の入口です。具体的な配置や用途は次の既存ページに整理しています。

調べたいこと子ページ
URLからビューへつなぐURLディスパッチャー(ルーティング処理)
GET/POSTの入力を受け取るGETとPOSTパラメータ受け取り
関数をファイルへ分ける関数ベースビューの作り方とviewの分割
クラスをファイルへ分けるクラスベースビューの作り方とviewの分割
CBVでHTTPメソッドを扱うクラスベースビューでGET/POSTリクエストの受け取り方
cookieを扱うcookieの値の設定と取得
レスポンスのステータスを選ぶHTTPステータスコードの返し方
別のページへ送るビューでリダイレクト
テンプレートへデータを渡すクラスベースビューでテンプレートに値を渡す方法

Function-based View (FBV) — 最も基本

旧コードの注意:次はモデル・テンプレート等を省略した旧断片です。createはrequest.POSTをそのまま保存し、欠落キーで例外にもなります。認証・操作権限・入力検証・対象範囲が含まれないため、そのまま公開しません。具体的なフォームと所有者の例をこの記事で分けて示します。

# myapp/views.py
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from .models import Article

# 最小の View
def hello(request):
    return HttpResponse("Hello, Django!")

# テンプレートをレンダー
def index(request):
    articles = Article.objects.all()
    return render(request, "myapp/index.html", {"articles": articles})

# 詳細ページ
def detail(request, article_id):
    article = get_object_or_404(Article, pk=article_id)
    return render(request, "myapp/detail.html", {"article": article})

# POST 処理 + リダイレクト
def create(request):
    if request.method == "POST":
        Article.objects.create(
            title=request.POST["title"],
            body=request.POST["body"],
        )
        return redirect("myapp:index")
    return render(request, "myapp/create.html")

追加教材:フォームで検証するFBV

次はhub_demo/fbv.pyです。フォームとモデルは「追加教材のモデルとフォーム」、URLとテンプレートは「追加教材のURLとテンプレート」に掲載します。既存myappの旧断片へ連結しないでください。

from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.views.decorators.http import require_http_methods
from .forms import ArticleForm


@login_required
@permission_required("hub_demo.add_article", raise_exception=True)
@require_http_methods(["GET", "POST"])
def create_article(request):
    form = ArticleForm(request.POST if request.method == "POST" else None)
    if request.method == "POST" and form.is_valid():
        article = form.save(commit=False)
        article.owner = request.user
        article.save()
        return redirect("hub_demo:index")
    return render(request, "hub_demo/article_form.html", {"form": form})

ログイン→add_article権限→GET/POST許可を確認し、有効なPOSTだけ保存します。空入力のGETは未バインド、空のPOSTはバインド済みの無効フォームで、request.POST or Noneのように空POSTをGET扱いしません。ownerは送信値でなくrequest.userを使います。POST後のredirectはブラウザの通常再送を避ける流れで、二重送信・同時更新の完全防止ではありません。

URL マッピング

旧コードの注意:次は旧myapp向けのURL断片です。対応するモデル/テンプレートが必要で、ここへのURL登録だけで認証や権限は追加されません。追加教材のhub_demoとは別の例です。既存URLconfを全体ごと置き換えません。

# myapp/urls.py
from django.urls import path
from . import views

app_name = "myapp"
urlpatterns = [
    path("", views.index, name="index"),
    path("hello/", views.hello, name="hello"),
    path("article/<int:article_id>/", views.detail, name="detail"),
    path("create/", views.create, name="create"),
]

# プロジェクト全体の urls.py
# project/urls.py
from django.urls import include, path
urlpatterns = [
    path("myapp/", include("myapp.urls")),
]

追加教材のモデルとフォーム

hub_demoは独立した検証用アプリです。本人の記事だけを扱う方針で、Articleにはowner/title/body/created_atを定義します。ownerやIDをフォームのfieldsへ含めません。実運用のモデルや権限管理を丸ごと置き換える例ではありません。

hub_demo/models.py

from django.conf import settings
from django.db import models


class Article(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    title = models.CharField(max_length=200)
    body = models.TextField(max_length=4000)
    created_at = models.DateTimeField(auto_now_add=True)

hub_demo/forms.py

from django import forms
from .models import Article


class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body"]

ModelFormは空/文字数等を検証しますが、fieldsに無いownerへの操作権限までは決めません。下記のビュー例でログイン・モデルの操作権限・ownerの取得範囲を別々に確認します。DBへ直接書く別経路や別APIまでこのチェックで守られるわけではありません。

Class-based View (CBV)

クラスベースは再利用性が高く、ミックスインで認証・権限・ページネーションを足せます。

旧コードの注意:次のHelloView.postは保存を実装せずリダイレクトするだけです。302が返っても保存成功の証拠にはなりません。クラスをURLへ渡す場合はas_view()で呼出し先を作り、GET/POST等のメソッドと認証/CSRFを別に確認します。

# myapp/views.py
from django.views import View
from django.shortcuts import render, redirect
from .models import Article

class HelloView(View):
    def get(self, request):
        return render(request, "myapp/hello.html")

    def post(self, request):
        # POST 処理
        return redirect("myapp:index")

# urls.py 側
# path("hello/", views.HelloView.as_view(), name="hello"),

ジェネリック CBV — 共通の処理を再利用

CRUD で頻出のパターンを Django が用意してくれています:

ジェネリック View用途テンプレート規約
ListView一覧app/model_list.html
DetailView詳細app/model_detail.html
CreateView新規作成app/model_form.html
UpdateView編集app/model_form.html
DeleteView削除app/model_confirm_delete.html
TemplateViewテンプレート表示。contextで動的な値も渡せる任意
RedirectViewリダイレクトテンプレート不要

旧コードの注意:次の旧CRUD例は認証・モデル権限・所有者範囲・フォーム/テンプレートを省略しています。DeleteViewは削除するビューで、URL公開だけでもアクセス制限にはなりません。orderingのcreated_atだけでは同時刻のページ順が確定しません。追加教材は操作権限とownerの絞込み、-pkの同点順を示します。

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Article

class ArticleListView(ListView):
    model = Article
    paginate_by = 20                 # ページネーション
    ordering = ["-created_at"]        # 降順

class ArticleDetailView(DetailView):
    model = Article

class ArticleCreateView(CreateView):
    model = Article
    fields = ["title", "body"]
    success_url = reverse_lazy("myapp:index")

class ArticleUpdateView(UpdateView):
    model = Article
    fields = ["title", "body"]
    success_url = reverse_lazy("myapp:index")

class ArticleDeleteView(DeleteView):
    model = Article
    success_url = reverse_lazy("myapp:index")

追加教材:操作権限と対象範囲を分けるCBV

hub_demo/views.py

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from .forms import ArticleForm
from .models import Article


class OwnerScopeMixin:
    def get_queryset(self):
        return super().get_queryset().filter(owner=self.request.user)


class ArticleListView(LoginRequiredMixin, OwnerScopeMixin, ListView):
    model = Article
    paginate_by = 20
    ordering = ["-created_at", "-pk"]


class ArticleDetailView(LoginRequiredMixin, OwnerScopeMixin, DetailView):
    model = Article


class ArticleCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    permission_required = "hub_demo.add_article"
    success_url = reverse_lazy("hub_demo:index")

    def form_valid(self, form):
        form.instance.owner = self.request.user
        return super().form_valid(form)


class ArticleUpdateView(LoginRequiredMixin, PermissionRequiredMixin, OwnerScopeMixin, UpdateView):
    model = Article
    form_class = ArticleForm
    permission_required = "hub_demo.change_article"
    success_url = reverse_lazy("hub_demo:index")


class ArticleDeleteView(LoginRequiredMixin, PermissionRequiredMixin, OwnerScopeMixin, DeleteView):
    model = Article
    permission_required = "hub_demo.delete_article"
    http_method_names = ["get", "post", "head", "options"]
    success_url = reverse_lazy("hub_demo:index")

LoginRequiredMixinを先頭に置き、PermissionRequiredMixinでadd/change/deleteのモデル権限を確認します。通常のModelBackendでモデル権限を持つだけでは「他人の記事を操作しない」という条件にならないため、一覧/詳細/更新/削除のget_querysetをownerで絞ります。対象外のIDは404、ログイン済みで操作権限が無ければ403になります。未ログインは既存のログイン画面へ送る前提で、この教材にログイン画面やユーザー発行処理は含めません。既定権限の用意/付与も別途必要です。

DeleteViewのGETは確認画面で、実際の削除は有効なPOSTです。この例ではDELETEメソッドも明示的に許可していません。サンプルの削除は物理削除で、ゴミ箱・監査・復旧・論理削除・排他制御や二重処理防止は実装していません。既存DBの行へ試さず、保持や削除承認が必要な業務へそのまま採用しません。各リクエストの許可は別経路も含めて設計します。

レスポンスの種類

get_object_or_404は成功時にモデルを返すhelperで、render/JsonResponse等のHTTP応答とは別です。GETのクエリやformの値を任意のredirect先にせず、固定の名前付きURLを使います。相対/外部URLを扱う場合は送り先の許可条件が必要です。

関数 / クラス用途
HttpResponse(text)素のレスポンス
render(request, template, context)HTML テンプレートをレンダー
redirect(name_or_url)リダイレクト。既定302で、指定/対象版によって異なる
JsonResponse({...})JSON 返却 (Content-Type 自動)
HttpResponseNotFound404
HttpResponseForbidden403
get_object_or_404(Model, pk=id)取得成功ならモデル、未取得ならHttp404を送出するhelper
FileResponse(open(...))ファイルダウンロード

デコレータで認証 / メソッド制限

旧コードの注意:次のpass関数は応答を返さない省略断片で、呼び出すと適切なHttpResponseを返せません。csrf_exemptはimportだけではCSRFを解除しませんが、解決策としてデコレーターを足しません。LoginRequiredMixin等の順序も重要で、モデル権限だけでは個別データへの権限は保証されません。

from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_http_methods, require_POST, require_GET
from django.views.decorators.csrf import csrf_exempt

@login_required
def my_page(request):
    return render(request, "myapp/mypage.html")

@require_POST
def post_only(request):
    # GET でアクセスすると 405 Method Not Allowed
    pass

@require_http_methods(["GET", "POST"])
@login_required
def secure_view(request):
    pass

@permission_required("myapp.delete_article")
def admin_only(request):
    pass

# CBV の場合
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class SecureView(LoginRequiredMixin, View):
    login_url = "/login/"

    def get(self, request):
        return render(request, "myapp/secure.html")

追加教材のURLとテンプレート

既存のauth/contenttypesとセッション・AuthenticationMiddleware・CsrfViewMiddleware・テンプレート探索を維持し、hub_demoを登録します。これは設定一式や本番配備の手順ではありません。hub_demo/__init__.pyは空にし、ROOT_URLCONFをhub_demo.project_urlsとした独立検体で確認しています。既存のURLconfやアプリ一覧を上書きせず、ログイン先も既存の安全な画面に合わせます。

hub_demo/urls.py

from django.urls import path
from .fbv import create_article
from .views import (ArticleListView, ArticleDetailView, ArticleCreateView,
                    ArticleUpdateView, ArticleDeleteView)

app_name = "hub_demo"
urlpatterns = [
    path("", ArticleListView.as_view(), name="index"),
    path("new/", ArticleCreateView.as_view(), name="create"),
    path("new-function/", create_article, name="create-fbv"),
    path("<int:pk>/", ArticleDetailView.as_view(), name="detail"),
    path("<int:pk>/edit/", ArticleUpdateView.as_view(), name="update"),
    path("<int:pk>/delete/", ArticleDeleteView.as_view(), name="delete"),
]

hub_demo/project_urls.py

from django.urls import include, path

urlpatterns = [path("articles/", include("hub_demo.urls"))]

hub_demo/templates/hub_demo/article_form.html

<!doctype html>
<html lang="ja"><meta charset="utf-8"><title>Article Form</title>
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Save</button>
</form>
</html>

hub_demo/templates/hub_demo/article_list.html

<!doctype html>
<html lang="ja"><meta charset="utf-8"><title>My Articles</title>
<ul>{% for article in object_list %}<li>{{ article.title }}</li>{% empty %}<li>Empty</li>{% endfor %}</ul>
{% if page_obj.has_previous %}<a href="?page={{ page_obj.previous_page_number }}">Previous</a>{% endif %}
{% if page_obj.has_next %}<a href="?page={{ page_obj.next_page_number }}">Next</a>{% endif %}
</html>

hub_demo/templates/hub_demo/article_detail.html

<!doctype html>
<html lang="ja"><meta charset="utf-8"><title>Article</title>
<h1>{{ object.title }}</h1>
<p>{{ object.body }}</p>
</html>

hub_demo/templates/hub_demo/article_confirm_delete.html

<!doctype html>
<html lang="ja"><meta charset="utf-8"><title>Confirm Deletion</title>
<p>Delete {{ object.title }}?</p>
<form method="post">{% csrf_token %}<button type="submit">Delete</button></form>
</html>

formと削除確認はcsrf_tokenを含め、表示値は通常のHTMLエスケープに任せます。safe/autoescape offでユーザー文字列をHTML化しません。CSRFと認証、モデル権限、対象行の範囲は別の確認です。request.userの利用には認証middlewareが必要で、ヘッダーやフォームからユーザーを決めません。

FBV vs CBV — どちらを選ぶ?

以下は構造の違いを見る観点で、人気調査や性能実測の順位ではありません。FBVも関数/デコレーターで共通化でき、CBVが自動で安全・高速になるわけではありません。

観点FBVCBV
学習コスト関数・デコレーター・分岐を追うdispatch・継承・MROも追う
コード量要件と共通化次第汎用処理と要件が合う場合は短くできる
柔軟性条件分岐を明示しやすい拡張hookと継承関係を確認する
再利用関数やデコレーターでも共通化できるmixin等で共通化できる
おすすめ独自処理、API エンドポイント典型的な CRUD

FAQ

Q: View で DB を直接叩いて良い?
A: 単純な処理なら OK。複雑なビジネスロジックは services.py 等に切り出して View は薄く保つのが定石。

Q: API エンドポイントは何を使う?
A: Django REST Framework の APIView / ViewSet を使うとシリアライザ・パーミッション・ページネーションが揃って便利。

Q: 404 と 500 を独自テンプレートにするには?
A: Djangoの標準エラービューを使う場合、探索できる場所に404.html/500.htmlを用意し、DEBUG=Falseの隔離環境で実際の404/500応答を確認します。プロジェクト側templatesならTEMPLATESのDIRSも照合します。独自handlerや外側Webサーバーが応答する構成、詳細を表示するDEBUG=Trueとは分け、Host設定だけでテンプレート探索が完了するとは扱いません。本番で故意に例外を起こしません。

根拠と検証範囲(2026年10月26日)

Django公式:ビュー、汎用編集ビュー、認証/モデル権限とmixin、ModelFormの許可項目、render/redirect/取得helper、MVC/MVTの呼称を確認しました。

追加教材はPython3.12.10/Django5.2.18で、新しいSQLite検体と仮ユーザー/権限だけを作って検査しています。匿名/権限不足/他人のID、CSRF、空/長すぎる入力、owner偽装、GET確認/POST更新、HTMLエスケープ、同時刻のページ順を確認。既存DBへの接続・migration・本番設定/認証変更は行っていません。ログイン画面や実パスワード認証・並行更新/本番DB/ASGI/全middleware・保持/監査の要件は未検証であり、この教材だけで本番CRUDが完成するという意味ではありません。

子ページ
  1. クラスベースビューの作り方とviewの分割
  2. 関数ベースビューの作り方とviewの分割
  3. URLディスパッチャー(ルーティング処理)
  4. GETとPOSTパラメータ受け取り
  5. クラスベースビューでGET/POSTリクエストの受け取り方
  6. クラスベースビューでテンプレートに値を渡す方法
  7. ビューでリダイレクト
  8. cookieの値の設定と取得
  9. HTTPステータスコードの返し方
同階層のページ
  1. ビュー(View)
  2. テンプレート(Template)
  3. モデル(Model)
  4. ルーティングの作成
  5. viewからtemplateへの遷移方法

最近更新/作成されたページ