DjangoによるWeb開発入門:ルーティング、テンプレート、ORMの基礎

Djangoプロジェクトを構築する際、ウェブアプリケーションの基盤となる要素には、環境設定からURLの処理、データの永続化まで多岐にわたります。本稿では、Django開発の主要な構成要素であるルーティング、ビュー、テンプレート、ORM(Object-Relational Mapping)、そして基本的なセッション管理について解説します。

開発環境のセットアップと初期設定

Djangoプロジェクトを開始するにあたり、まず仮想環境を設定し、必要なパッケージをインストールします。

# 仮想環境の作成
python -m venv myproject_env

# 仮想環境のアクティベート (Windowsの場合)
myproject_env\Scripts\activate

# Djangoのインストール (特定のバージョンを指定)
pip install django==4.2.0

プロジェクトの構成要素として、URLの定義、ビュー関数の作成、そしてルーティング設定が挙げられます。これらは、ユーザーからのリクエストを適切に処理し、レスポンスを返すための基本的な枠組みとなります。

URLルーティングの基礎

Djangoでは、urls.pyファイルを使用してURLパターンとそれに対応するビュー関数をマッピングします。主な関数としてpath()とre_path()があります。

path()によるURLマッピング

path()関数は、シンプルなURLパターンを定義するのに使われます。URLの一部を動的に取り込むには「パスコンバータ」を利用します。

# project_name/urls.py
from django.contrib import admin
from django.urls import path
from myapp import views # myappというアプリケーションのviews.pyをインポート

urlpatterns = [
    path('admin/', admin.site.urls),
    # 静的なURLパターン
    path('greeting/hello/', views.hello_view),
    # パスコンバータを使用した動的なURLパターン
    # 例: /article/100 にアクセスすると、ビューに100が渡される
    path('article//', views.article_detail_view),
]

myapp/views.pyでのビュー関数の定義例です。

# myapp/views.py
from django.http import HttpResponse

def hello_view(request):
    return HttpResponse("こんにちは!これは静的なページです。")

def article_detail_view(request, article_id):
    # パスコンバータで抽出された値(article_id)を利用
    html_content = f"<h3>記事番号: {article_id} の詳細ページ</h3>"
    return HttpResponse(html_content)

path()のマッチングは上から順に行われ、最初に一致したパターンが採用されます。

re_path()による正規表現ルーティング

より複雑なURLパターンを扱う場合、正規表現を使用するre_path()関数が有効です。

# project_name/urls.py
from django.urls import path, re_path
from myapp import views

urlpatterns = [
    # ... 他のパス ...
    # 正規表現を使用して、2桁までの整数と特定の演算子を組み合わせたURLを定義
    # 例: /calculate/12/plus/34 にアクセス
    re_path(r'^calculate/(?P<num1>\d{1,2})/(?P<operation>\w+)/(?P<num2>\d{1,2})/$', views.calculator_view),
    # 誕生日フォーマットのURL(YYYY/MM/DDまたはMM/DD/YYYY)
    re_path(r'^event/date/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', views.event_date_view),
]

myapp/views.pyでのビュー関数の定義例です。

# myapp/views.py
from django.http import HttpResponse

def calculator_view(request, num1, operation, num2):
    n1 = int(num1)
    n2 = int(num2)
    result = 0
    message = ""

    if operation == 'plus':
        result = n1 + n2
    elif operation == 'minus':
        result = n1 - n2
    elif operation == 'multiply':
        result = n1 * n2
    elif operation == 'divide':
        if n2 != 0:
            result = n1 / n2
        else:
            message = "0での除算はできません。"
    else:
        message = "サポートされていない演算子です。"
        return HttpResponse(f"エラー: {message}")

    if message:
        return HttpResponse(f"エラー: {message}")
    else:
        return HttpResponse(f"計算結果: {n1} {operation} {n2} = {result}")

def event_date_view(request, year, month, day):
    return HttpResponse(f"指定されたイベント日付: {year}年{month}月{day}日")

HTTPリクエストとレスポンス

ウェブアプリケーションは、クライアントからのHTTPリクエストを受け取り、処理後にHTTPレスポンスを返します。

リクエストオブジェクト (HttpRequest)

ビュー関数にはHttpRequestオブジェクトが渡され、リクエストに関する様々な情報にアクセスできます。

  • request.method: リクエストメソッド (例: 'GET', 'POST')
  • request.path_info: リクエストされたパスの情報
  • request.GET: GETパラメータを辞書のように扱えるオブジェクト (QueryDict)
  • request.POST: POSTパラメータを辞書のように扱えるオブジェクト (QueryDict)
  • request.META: リクエストのメタデータ (HTTPヘッダーなど)
  • request.COOKIES: クライアントから送信されたCookieを辞書のように扱えるオブジェクト
# myapp/views.py
from django.http import HttpResponse

def handle_request(request):
    print(f"パス情報: {request.path_info}")
    print(f"メソッド: {request.method}")
    print(f"GETパラメータ: {request.GET}")
    print(f"フルパス: {request.get_full_path()}")
    # 例: クライアントIPアドレス
    print(f"リモートアドレス: {request.META.get('REMOTE_ADDR')}")

    if request.method == 'GET':
        param_a = request.GET.get('param_a', '未指定')
        list_items = request.GET.getlist('items') # 同じキーで複数値を取得
        return HttpResponse(f"GETリクエストを受信しました。param_a: {param_a}, items: {list_items}")
    elif request.method == 'POST':
        user_name = request.POST.get('username', '匿名')
        return HttpResponse(f"POSTリクエストを受信しました。ユーザー名: {user_name}")
    else:
        return HttpResponse(f"サポートされていないメソッド: {request.method}")
# project_name/urls.py
from django.urls import path
from myapp import views

urlpatterns = [
    # ...
    path('process_data/', views.handle_request),
]

HTMLフォームからPOSTリクエストを送信する例です。

<!-- templates/myapp/data_form.html -->
<form action="/process_data/" method="post">
    {% csrf_token %} <!-- DjangoのCSRF保護トークン -->
    <label for="username">ユーザー名:</label>
    <input type="text" id="username" name="username">
    <button type="submit">送信</button>
</form>

注意: DjangoはデフォルトでCSRF (Cross-Site Request Forgery) 保護を有効にしています。POSTフォームには{% csrf_token %}を記述する必要があります。開発段階で一時的に無効にする場合は、settings.pyのMIDDLEWAREから'django.middleware.csrf.CsrfViewMiddleware'をコメントアウトできますが、本番環境では決して推奨されません。

レスポンスオブジェクト (HttpResponse)

サーバーからクライアントへ返されるHTTPレスポンスは、HttpResponseオブジェクトで構築されます。

# myapp/views.py
from django.http import HttpResponse

def simple_response(request):
    # レスポンスボディとステータスコードを指定
    return HttpResponse("成功しました。", status=200, content_type="text/plain")

def redirect_example(request):
    from django.http import HttpResponseRedirect
    # 他のURLへリダイレクト
    return HttpResponseRedirect("/greeting/hello/")

一般的なHTTPステータスコード:

  • 200 OK: リクエスト成功
  • 301 Moved Permanently: 恒久的なリダイレクト
  • 302 Found (または Moved Temporarily): 一時的なリダイレクト
  • 404 Not Found: リソースが見つからない
  • 500 Internal Server Error: サーバー内部エラー

Djangoテンプレートシステム

Djangoは、動的なHTMLページを生成するための強力なテンプレートシステムを提供します。MVC (Model-View-Controller) パターンのバリエーションであるMTV (Model-Template-View) パターンに従います。

  • Model: データ構造とデータベースとの相互作用を定義します。
  • Template: ユーザーに表示されるUIロジックとコンテンツを定義します。
  • View: リクエストを処理し、モデルとテンプレートを連携させてレスポンスを生成します。

テンプレートの作成と読み込み

テンプレートは通常、プロジェクトルートまたは各アプリケーション内のtemplatesディレクトリに配置します。

  1. プロジェクトルートにtemplatesフォルダを作成。
  2. settings.pyのTEMPLATES設定でDIRSにこのフォルダのパスを追加。
    # settings.py
    TEMPLATES = [
        {
            # ...
            'DIRS': [os.path.join(BASE_DIR, 'templates')], # ここに追加
            # ...
        },
    ]
  3. ビュー関数からテンプレートをレンダリング。

テンプレートをレンダリングする方法には、loaderモジュールを使う方法と、render()ショートカット関数を使う方法があります。

loaderモジュールを使用する例

# myapp/views.py
from django.template import loader
from django.http import HttpResponse

def show_product_list_loader(request):
    template = loader.get_template('myapp/product_list.html')
    context = {
        'products': [
            {'name': 'Laptop', 'price': 1200},
            {'name': 'Mouse', 'price': 25},
        ]
    }
    html = template.render(context, request) # contextとrequestを渡す
    return HttpResponse(html)

render()ショートカット関数を使用する例 (推奨)

# myapp/views.py
from django.shortcuts import render

def show_product_list_render(request):
    context = {
        'products': [
            {'name': 'Keyboard', 'price': 75},
            {'name': 'Monitor', 'price': 300},
        ],
        'app_name': 'MyAwesomeShop'
    }
    return render(request, 'myapp/product_list.html', context) # request, テンプレート名, context
<!-- templates/myapp/product_list.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ app_name }} - 商品リスト</title>
</head>
<body>
    <h1>商品一覧</h1>
    <ul>
        {% for product in products %}
            <li>{{ product.name }}: ¥{{ product.price }}</li>
        {% empty %}
            <li>商品はありません。</li>
        {% endfor %}
    </ul>
</body>
</html>

テンプレート変数とタグ

ビュー関数からテンプレートに渡されたデータは「コンテキスト」と呼ばれ、テンプレート内で{{ variable_name }}構文を使ってアクセスできます。

テンプレートで扱えるデータ型は、文字列、整数、リスト、タプル、辞書、関数、クラスのインスタンスなど多岐にわたります。

# myapp/views.py
from django.shortcuts import render

def demonstration_view(request):
    class Pet:
        def __init__(self, name):
            self.name = name
        def bark(self):
            return f"{self.name}はワンワン吠える!"

    def get_current_year():
        from datetime import datetime
        return datetime.now().year

    context = {
        'simple_string': 'Hello Django!',
        'number_value': 123,
        'item_list': ['Apple', 'Banana', 'Cherry'],
        'user_info': {'name': 'Taro', 'age': 30},
        'callable_func': get_current_year,
        'pet_object': Pet('Pochi'),
    }
    return render(request, 'myapp/demo_template.html', context)
<!-- templates/myapp/demo_template.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>テンプレートデモ</title>
</head>
<body>
    <h3>文字列: {{ simple_string }}</h3>
    <h3>数値: {{ number_value }}</h3>
    <h3>リスト全体: {{ item_list }}</h3>
    <h3>リストの最初の要素: {{ item_list.0 }}</h3>
    <h3>辞書全体: {{ user_info }}</h3>
    <h3>辞書の'name'キー: {{ user_info.name }}</h3> <!-- ドット記法でキーにアクセス -->
    <h3>関数呼び出し: {{ callable_func }}</h3>
    <h3>オブジェクトのメソッド呼び出し: {{ pet_object.bark }}</h3>
</body>
</html>

テンプレートタグ: {% tag %}構文は、条件分岐やループなどの制御構造をテンプレートに埋め込むために使われます。

<!-- templates/myapp/conditional_loop.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>条件とループ</title>
</head>
<body>
    {% if score >= 80 %}
        <p>素晴らしい!合格です。</p>
    {% elif score >= 60 %}
        <p>もう少し頑張りましょう。</p>
    {% else %}
        <p>残念ながら不合格です。</p>
    {% endif %}

    <h3>参加者リスト</h3>
    <ul>
        {% for participant in participant_list %}
            {% if forloop.first %}<li>--- リスト開始 ---</li>{% endif %}
            <li>{{ forloop.counter }}: {{ participant.name }} (年齢: {{ participant.age }})</li>
            {% if forloop.last %}<li>--- リスト終了 ---</li>{% endif %}
        {% empty %}
            <li>参加者はいません。</li>
        {% endfor %}
    </ul>
</body>
</html>

forloopオブジェクトは、ループの現在のイテレーションに関する情報を提供します (例: forloop.first, forloop.last, forloop.counter)。

テンプレートフィルター

フィルターは、変数の値を表示時に加工するために使用されます。{{ variable|filter_name:argument }}という構文で適用します。

  • {{ my_string|lower }}: 文字列をすべて小文字に変換
  • {{ my_string|upper }}: 文字列をすべて大文字に変換
  • {{ html_content|safe }}: HTMLエスケープを行わない (危険なHTMLを表示する可能性があるため注意)
  • {{ number|add:"5" }}: 数値に指定した値を加算
  • {{ long_text|truncatechars:"20" }}: 指定された文字数で文字列を切り詰め、末尾に「...」を追加
<!-- templates/myapp/filters_example.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>フィルターデモ</title>
</head>
<body>
    <p>元の文字列: {{ my_text }}</p>
    <p>小文字変換: {{ my_text|lower }}</p>
    <p>大文字変換: {{ my_text|upper }}</p>
    <p>数値加算 (10 + 5): {{ 10|add:"5" }}</p>
    <p>切り詰め: {{ long_description|truncatechars:"30" }}</p>
    <p>HTMLエスケープなし (安全でない場合がある): {{ raw_html|safe }}</p>
</body>
</html>

テンプレートの継承

テンプレート継承は、共通のレイアウトを持つ複数のページを作成する際に、コードの重複を避けるための強力な機能です。親テンプレートで{% block block_name %}を定義し、子テンプレートでそれを{% extends 'parent.html' %}と{% block block_name %} ... {% endblock %}でオーバーライドします。

<!-- templates/base.html (親テンプレート) -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}デフォルトタイトル{% endblock %}</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        .header { background-color: #f0f0f0; padding: 10px; }
        .content { margin-top: 20px; border: 1px solid #ccc; padding: 15px; }
        .footer { margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; font-size: 0.8em; text-align: center; }
    </style>
</head>
<body>
    <div class="header">
        <h1>私のウェブサイト</h1>
        <nav>
            <a href="/">ホーム</a> |
            <a href="/news/">ニュース</a> |
            <a href="/about/">会社概要</a>
        </nav>
    </div>

    <div class="content">
        {% block main_content %}
        <p>ここにメインコンテンツが表示されます。</p>
        {% endblock %}
    </div>

    <div class="footer">
        &copy; 2023 My Website. All rights reserved.
    </div>
</body>
</html>
<!-- templates/news/index.html (子テンプレート) -->
{% extends 'base.html' %}

{% block title %}最新ニュース{% endblock %}

{% block main_content %}
    <h2>本日のトップニュース</h2>
    <p>詳細はこちらをご覧ください。</p>
    <ul>
        <li>記事1</li>
        <li>記事2</li>
    </ul>
{% endblock %}

URLの逆引き (URL Reversing)

URLの逆引きとは、ビュー関数やテンプレート内でURLパターンに割り当てられた「名前(name)」を使って、実際のURLパスを動的に生成する機能です。これにより、URLパターンを変更しても、コード内のリンクを修正する必要がなくなります。

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

urlpatterns = [
    path('homepage/', views.home_page, name='home'), # name='home'で逆引き可能
    path('products//', views.product_detail, name='product_detail'),
]

テンプレートでの逆引き

<!-- templates/myapp/some_page.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>URL逆引きのデモ</title>
</head>
<body>
    <p>ホームページへのリンク: <a href="{% url 'home' %}">こちら</a></p>
    <p>商品ID 105 の詳細ページ: <a href="{% url 'product_detail' product_id=105 %}">商品105</a></p>
</body>
</html>

ビュー関数内での逆引き

# myapp/views.py
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.shortcuts import render

def home_page(request):
    return HttpResponse("ようこそホームページへ!")

def product_detail(request, product_id):
    return HttpResponse(f"商品ID {product_id} の詳細です。")

def redirect_to_home(request):
    # 'home'という名前のURLパターンに対応するパスを逆引きしてリダイレクト
    url = reverse('home')
    return HttpResponseRedirect(url)

def show_some_page(request):
    return render(request, 'myapp/some_page.html')

静的ファイルの配信

CSS、JavaScript、画像などの静的ファイルは、ウェブアプリケーションの重要な要素です。Djangoでは、これらのファイルを効率的に管理し、配信する仕組みが提供されています。

  1. プロジェクトルートにstaticフォルダを作成。
  2. settings.pyでSTATICFILES_DIRSにこのフォルダのパスを追加。
    # settings.py
    STATIC_URL = '/static/'
    STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
  3. テンプレートで静的ファイルを使用。
<!-- templates/myapp/static_demo.html -->
{% load static %} <!-- staticタグを使用するためにロード -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>静的ファイルのデモ</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <h1>静的ファイルのテスト</h1>
    <img src="{% static 'images/sample_image.jpg' %}" alt="サンプル画像" width="200">
    <script src="{% static 'js/script.js' %}"></script>
</body>
<html>

staticディレクトリの内部構造は自由に決められます (例: static/css/, static/images/, static/js/)。

Djangoアプリケーションと分散ルーティング

Djangoプロジェクトは、複数の「アプリケーション」で構成することができます。アプリケーションは独立した機能モジュールであり、独自のモデル、ビュー、テンプレート、URL設定を持つことができます。

アプリケーションの作成

manage.pyスクリプトを使ってアプリケーションを作成します。

python manage.py startapp blog

作成後、settings.pyのINSTALLED_APPSリストに新しいアプリケーションを追加します。

# settings.py
INSTALLED_APPS = [
    # ...
    'blog', # 作成したアプリケーション
]

分散ルーティング

プロジェクトのメインのurls.pyでは、すべてのURLを直接処理するのではなく、各アプリケーションのurls.pyにルーティングを委譲することができます。

# project_name/urls.py (プロジェクトのメインURL設定)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')), # 'blog/'以下のURLをblogアプリのurls.pyに委譲
]
# blog/urls.py (blogアプリケーションのURL設定)
from django.urls import path
from . import views # blogアプリケーションのviews.pyをインポート

urlpatterns = [
    # 例: /blog/posts/ にアクセス
    path('posts/', views.post_list_view, name='post_list'),
    # 例: /blog/posts/5/ にアクセス
    path('posts//', views.post_detail_view, name='post_detail'),
]
# blog/views.py
from django.http import HttpResponse
from django.shortcuts import render

def post_list_view(request):
    return HttpResponse("ブログ記事の一覧です。")

def post_detail_view(request, post_id):
    return HttpResponse(f"ブログ記事ID {post_id} の詳細ページです。")

アプリケーションごとのテンプレート

各アプリケーションは独自のtemplatesディレクトリを持つことができます。settings.pyのTEMPLATES設定で'APP_DIRS': Trueを設定していれば、Djangoは自動的に各アプリケーション内のtemplatesフォルダを検索します。

テンプレートの解決順序:

  1. settings.TEMPLATES[0]['DIRS']に指定されたディレクトリ (プロジェクトルートのtemplates)
  2. INSTALLED_APPSにリストされた順序で、各アプリケーション内のtemplatesディレクトリ

名前の衝突を避けるため、通常はアプリケーション内のテンプレートをtemplates/app_name/template_file.htmlのようにサブディレクトリに配置することが推奨されます。

Django ORM (Object-Relational Mapping)

ORMは、Pythonオブジェクトを使ってデータベースと対話するための技術です。SQLクエリを直接書く代わりに、Pythonのクラスとオブジェクトを操作することで、データベースのレコードを作成、読み取り、更新、削除 (CRUD) できます。

データベースのセットアップ

DjangoはSQLiteをデフォルトで使用しますが、MySQLなどの他のデータベースもサポートしています。MySQLを使用する場合、settings.pyでデータベース接続情報を設定します。

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # MySQLを使用
        'NAME': 'mydjangodb',                 # データベース名
        'USER': 'root',                       # データベースユーザー名
        'PASSWORD': 'your_password',          # データベースパスワード
        'HOST': '127.0.0.1',                  # データベースホスト
        'PORT': '3306',                       # データベースポート
    }
}

MySQLを使用するには、mysqlclientなどのPythonパッケージをインストールする必要があります: pip install mysqlclient。

モデルの定義

モデルはdjango.db.models.Modelを継承するPythonクラスであり、データベースのテーブルとそのフィールドを表現します。

# myapp/models.py
from django.db import models

class Product(models.Model):
    name = models.CharField('商品名', max_length=100, default='')
    description = models.TextField('説明', blank=True, null=True)
    price = models.DecimalField('価格', max_digits=10, decimal_places=2)
    stock = models.IntegerField('在庫数', default=0)
    created_at = models.DateTimeField('作成日', auto_now_add=True)
    updated_at = models.DateTimeField('更新日', auto_now=True)
    is_available = models.BooleanField('販売中', default=True)

    class Meta:
        db_table = 'products' # データベースのテーブル名を指定
        verbose_name = '商品'
        verbose_name_plural = '商品'

    def __str__(self):
        return self.name

フィールドタイプとオプションの例

  • models.BooleanField(): 真偽値 (DBではTINYINT(1))
  • models.CharField(max_length=...): 短い文字列 (VARCHAR)。max_lengthは必須。
  • models.TextField(): 長い文字列 (LONGTEXT)。
  • models.IntegerField(): 整数 (INT)。
  • models.DecimalField(max_digits=..., decimal_places=...): 固定小数点数。max_digitsは数値の総桁数、decimal_placesは小数点以下の桁数。
  • models.DateField(auto_now_add=True/auto_now=True): 日付。auto_now_addはオブジェクト初回作成時、auto_nowは保存時に自動更新。
  • models.DateTimeField(): 日付と時刻。
  • models.EmailField(): メールアドレス (CharFieldのラッパー)。
  • models.ImageField(upload_to=...): 画像ファイル。

マイグレーション

モデル定義を変更したら、以下のコマンドでデータベーススキーマを同期します。

# マイグレーションファイルの作成 (モデル定義から変更を検出)
python manage.py makemigrations myapp

# データベースへの変更適用 (実際にテーブルを作成・変更)
python manage.py migrate

CRUD操作 (Create, Read, Update, Delete)

Django ORMは、MyModel.objectsマネージャを通じてデータベース操作を行います。これはDjango Shell (python manage.py shell) で試すのが便利です。

# python manage.py shell を実行
from myapp.models import Product

# --- データ作成 (Create) ---
# 方法1: オブジェクトを作成して保存
p1 = Product(name='デスクチェア', price=150.00, stock=50)
p1.save()

# 方法2: create() メソッドで一度に作成
p2 = Product.objects.create(name='PCモニター', price=300.00, stock=30, description='27インチ高解像度モニター')

print(f"作成された商品1: {p1.name}")
print(f"作成された商品2: {p2.name}")

# --- データ読み取り (Read) ---
# 全てのレコードを取得
all_products = Product.objects.all()
for p in all_products:
    print(f"商品: {p.name}, 価格: {p.price}")

# 特定のIDのレコードを取得 (存在しない場合や複数ヒットでエラー)
try:
    chair = Product.objects.get(id=1)
    print(f"ID 1 の商品: {chair.name}")
except Product.DoesNotExist:
    print("指定されたIDの商品は存在しません。")

# 条件に一致するレコードをフィルタリング
expensive_products = Product.objects.filter(price__gte=200.00) # price >= 200
for p in expensive_products:
    print(f"高額商品: {p.name}")

# 条件に一致しないレコードを除外
not_available_products = Product.objects.exclude(is_available=True)
print(f"販売停止中の商品数: {not_available_products.count()}")

# 特定のフィールドのみを選択 (辞書のリストを返す)
product_names_prices = Product.objects.values('name', 'price')
print(f"商品名と価格 (辞書): {list(product_names_prices)}")

# 特定のフィールドのみを選択 (タプルのリストを返す)
product_names_only = Product.objects.values_list('name', flat=True) # flat=Trueで単一フィールドのリスト
print(f"商品名のみ (タプル/リスト): {list(product_names_only)}")

# ソート
sorted_products = Product.objects.order_by('price') # 価格の昇順
reverse_sorted_products = Product.objects.order_by('-created_at') # 作成日の降順

# --- 非等値クエリ (Query Lookups) ---
# __exact: 完全一致 (デフォルトなので省略可能)
p = Product.objects.get(name__exact='デスクチェア')

# __contains: 部分文字列を含む (大文字小文字を区別)
monitors = Product.objects.filter(name__contains='モニター')

# __icontains: 部分文字列を含む (大文字小文字を区別しない)
monitors_case_insensitive = Product.objects.filter(name__icontains='モニター')

# __startswith, __endswith: 特定の文字列で始まる/終わる
starts_with_pc = Product.objects.filter(name__startswith='PC')

# __gt, __gte, __lt, __lte: より大きい、以上、より小さい、以下
products_over_100 = Product.objects.filter(price__gt=100.00)

# __in: 指定されたリストの値のいずれかに一致
selected_ids = [1, 3, 5]
specific_products = Product.objects.filter(id__in=selected_ids)

# __range: 指定された範囲内
products_in_range = Product.objects.filter(price__range=(50.00, 200.00))

# --- データ更新 (Update) ---
# 単一レコードの更新: 取得 -> 変更 -> 保存
chair_to_update = Product.objects.get(name='デスクチェア')
chair_to_update.price = 160.00
chair_to_update.save()
print(f"更新後のデスクチェア価格: {Product.objects.get(name='デスクチェア').price}")

# 複数レコードの一括更新
# 在庫が0の商品を全て販売停止にする
Product.objects.filter(stock=0).update(is_available=False)
print(f"販売停止中の商品数 (更新後): {Product.objects.filter(is_available=False).count()}")

# --- データ削除 (Delete) ---
# 単一レコードの削除: 取得 -> 削除
try:
    product_to_delete = Product.objects.get(name='古い在庫品')
    product_to_delete.delete()
    print("商品 '古い在庫品' を削除しました。")
except Product.DoesNotExist:
    print("削除対象の商品 '古い在庫品' は存在しません。")

# 複数レコードの一括削除
# 在庫が少ない商品をすべて削除
Product.objects.filter(stock__lt=10).delete()
print("在庫が10未満の商品を削除しました。")

# --- 論理削除 (Soft Delete) ---
# 実際の削除ではなく、フラグで非アクティブ化
# 例えば is_active=True のフィールドを追加し、削除時に False に設定
# 上記のProductモデルでは is_available を利用
p_to_soft_delete = Product.objects.get(name='PCモニター')
p_to_soft_delete.is_available = False
p_to_soft_delete.save()
print(f"PCモニターの販売状況: {Product.objects.get(name='PCモニター').is_available}")

FオブジェクトとQオブジェクト

Fオブジェクト: データベースのフィールド値をPythonで取得せずに直接操作したり、異なるフィールド間で比較したりする際に使用します。

from django.db.models import F

# 全商品の価格を10%値上げする
Product.objects.all().update(price=F('price') * 1.1)

# 在庫数よりも価格が高い商品を検索する
expensive_by_stock = Product.objects.filter(price__gt=F('stock'))
for p in expensive_by_stock:
    print(f"在庫より価格が高い商品: {p.name}, 価格: {p.price}, 在庫: {p.stock}")

Qオブジェクト: 複雑な論理条件 (AND, OR, NOT) を組み合わせたクエリを作成する際に使用します。

from django.db.models import Q

# 価格が200より低い、または在庫が50を超える商品を検索
filtered_products = Product.objects.filter(Q(price__lt=200) | Q(stock__gt=50))
for p in filtered_products:
    print(f"条件に合う商品: {p.name}, 価格: {p.price}, 在庫: {p.stock}")

# 価格が100より高く、かつ販売中の商品を検索
active_and_priced = Product.objects.filter(Q(price__gt=100) & Q(is_available=True))

# 特定の名前ではない商品を検索 (NOT)
not_desk_chair = Product.objects.exclude(Q(name='デスクチェア'))

集計クエリと生SQL

集計クエリ

aggregate()やannotate()メソッドを使用して、集計関数 (SUM, COUNT, AVG, MAX, MIN) を実行できます。

from django.db.models import Count, Sum, Avg, Max, Min

# 全商品の平均価格を計算
avg_price = Product.objects.aggregate(average_price=Avg('price'))
print(f"全商品の平均価格: {avg_price['average_price']}")

# 商品の総数をカウント
total_products = Product.objects.aggregate(total_count=Count('id'))
print(f"総商品数: {total_products['total_count']}")

生SQLクエリ

ORMで表現できない複雑なクエリが必要な場合、MyModel.objects.raw()を使用して生SQLを実行できます。ただし、SQLインジェクションのリスクがあるため慎重に扱う必要があります。

# from myapp.models import Product
raw_products = Product.objects.raw('SELECT id, name, price FROM products WHERE price > %s ORDER BY name', [100])
for p in raw_products:
    print(f"生SQLで取得: {p.name}, 価格: {p.price}")

Django Admin

Djangoには強力な管理サイトが組み込まれており、モデルを登録するだけでデータのCRUD操作を行うためのウェブインターフェースが自動的に生成されます。

管理ユーザーの作成

まず、管理サイトにログインするためのスーパーユーザーを作成します。

python manage.py createsuperuser

モデルの登録

admin.pyファイルでモデルを登録します。

# myapp/admin.py
from django.contrib import admin
from .models import Product

# Productモデルを管理サイトに登録
admin.site.register(Product)

これで、/admin/にアクセスし、作成したスーパーユーザーでログインすると、Productモデルのデータを管理できるようになります。

ModelAdminクラスによるカスタマイズ

管理サイトの表示や動作をより詳細に制御するには、ModelAdminクラスを継承したカスタムクラスを作成し、admin.site.register()に渡します。

# myapp/admin.py
from django.contrib import admin
from .models import Product

class ProductAdmin(admin.ModelAdmin):
    # リスト表示するフィールド
    list_display = ['id', 'name', 'price', 'stock', 'is_available', 'created_at']
    # リンクとして詳細ページに遷移するフィールド
    list_display_links = ['name']
    # フィルターとして使用するフィールド (サイドバーに表示)
    list_filter = ['is_available', 'created_at']
    # 検索ボックスで検索対象とするフィールド
    search_fields = ['name', 'description']
    # リストページで直接編集可能なフィールド
    list_editable = ['price', 'stock', 'is_available']
    # ページネーションあたりの表示数
    list_per_page = 20

admin.site.register(Product, ProductAdmin)

リレーションシップ (関係性)

リレーショナルデータベースでは、異なるテーブル間に関係性を設定できます。Django ORMでは、これらの関係性をモデルのフィールドで表現します。

One-to-Many (一対多)

一つのレコードが複数の他のレコードに関連付けられる関係です。例: 一つのカテゴリに複数の商品が属する。

多側のモデル (Product) にForeignKeyフィールドを設定します。

# myapp/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField('カテゴリ名', max_length=50, unique=True)

    def __str__(self):
        return self.name

class Product(models.Model):
    # ... (上記Productモデルのフィールド) ...
    category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name='カテゴリ')
    # on_delete: 関連するCategoryが削除されたときの動作を定義
    #   - CASCADE: Category削除時にProductも削除
    #   - PROTECT: CategoryにProductが紐づいている場合、削除を禁止
    #   - SET_NULL: Category削除時にProductのcategory_idをNULLに設定 (null=Trueが必要)
    #   - SET_DEFAULT: Category削除時にProductのcategory_idをデフォルト値に設定 (defaultが必要)
    #   - DO_NOTHING: 何もしない (データベースで参照整合性エラーが発生する可能性あり)

    def __str__(self):
        return self.name

関連するオブジェクトへのアクセス:

# python manage.py shell
from myapp.models import Category, Product

# カテゴリと商品を作成
electronics = Category.objects.create(name='エレクトロニクス')
books = Category.objects.create(name='書籍')

product_laptop = Product.objects.create(name='高性能ノートPC', price=1200.00, category=electronics)
product_keyboard = Product.objects.create(name='メカニカルキーボード', price=150.00, category=electronics)
product_django_book = Product.objects.create(name='Django入門書', price=40.00, category=books)

# 商品からカテゴリへアクセス
print(f"ノートPCのカテゴリ: {product_laptop.category.name}")

# カテゴリから関連する商品へアクセス (リバースリレーション)
# デフォルトで category_set という名前になる (related_nameで変更可能)
electronics_products = electronics.product_set.all()
print(f"エレクトロニクス関連商品:")
for p in electronics_products:
    print(f" - {p.name}")

Many-to-Many (多対多)

両方のモデルが互いに複数のレコードと関連付けられる関係です。例: 一つの記事に複数のタグが付けられ、一つのタグが複数の記事に付けられる。

どちらかのモデルにManyToManyFieldフィールドを設定します (どちらに設定しても同じ結果になりますが、意味的に自然な方を選ぶと良いでしょう)。

# myapp/models.py
from django.db import models

class Tag(models.Model):
    name = models.CharField('タグ名', max_length=50, unique=True)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField('タイトル', max_length=200)
    content = models.TextField('内容')
    tags = models.ManyToManyField(Tag, verbose_name='タグ') # ManyToManyFieldを定義

    def __str__(self):
        return self.title

関連するオブジェクトへのアクセス:

# python manage.py shell
from myapp.models import Tag, Post

# タグと記事を作成
tag_python = Tag.objects.create(name='Python')
tag_web = Tag.objects.create(name='Web開発')
tag_django = Tag.objects.create(name='Django')

post1 = Post.objects.create(title='Djangoでブログを構築する', content='...')
post2 = Post.objects.create(title='Pythonの基礎', content='...')

# 記事にタグを追加
post1.tags.add(tag_web, tag_django)
post2.tags.add(tag_python)
post2.tags.add(tag_web)

# 記事からタグへアクセス
print(f"'{post1.title}' のタグ:")
for tag in post1.tags.all():
    print(f" - {tag.name}")

# タグから関連する記事へアクセス (リバースリレーション)
print(f"'{tag_django.name}' タグが付いた記事:")
for post in tag_django.post_set.all(): # デフォルトで post_set になる
    print(f" - {post.title}")

CookiesとSession

HTTPはステートレスなプロトコルであるため、ユーザーの状態を維持するためにCookieとSessionが使われます。

Cookies

Cookieは、クライアント (ブラウザ) 側に保存される小さなデータです。キーと値のペアで構成され、有効期限を持つことができます。

  • ドメインごとに隔離され、異なるドメインのCookieにはアクセスできない。
  • リクエストごとにサーバーへ送信されるため、サイズが大きすぎるとパフォーマンスに影響を与える可能性がある。

Cookieの設定

# myapp/views.py
from django.http import HttpResponse

def set_user_cookie(request):
    response = HttpResponse("Cookieを設定しました。")
    # 'username'という名前で'john_doe'という値を設定し、有効期限を600秒 (10分) にする
    response.set_cookie('username', 'john_doe', max_age=600)
    return response

Cookieの取得

# myapp/views.py
from django.http import HttpResponse

def get_user_cookie(request):
    username = request.COOKIES.get('username') # 'username'キーのCookieを取得
    if username:
        return HttpResponse(f"あなたのユーザー名は: {username}")
    else:
        return HttpResponse("Cookie 'username' が見つかりません。")

Session

Sessionは、サーバー側に保存されるユーザー固有のデータです。クライアントにはSession IDのみがCookieとして保存され、このIDを使ってサーバーが対応するSessionデータを識別します。

  • 各クライアントはサーバー上で独立したSessionを持つ。
  • 機密性の高いデータをサーバーに保存できる。
  • Djangoはデフォルトでデータベースバックエンドを使ってSessionを管理する。

Sessionの使用

HttpRequestオブジェクトのsession属性を通じて、辞書のようにSessionデータを操作できます。

# myapp/views.py
from django.http import HttpResponse

def set_session_data(request):
    # Sessionに値を保存
    request.session['user_id'] = 123
    request.session['is_logged_in'] = True
    request.session['cart_items'] = ['Item A', 'Item B'] # リストも保存可能
    return HttpResponse("Sessionデータを設定しました。")

def get_session_data(request):
    user_id = request.session.get('user_id', '未設定') # デフォルト値を指定して取得
    is_logged_in = request.session.get('is_logged_in', False)
    cart_items = request.session.get('cart_items', [])
    return HttpResponse(f"ユーザーID: {user_id}, ログイン済み: {is_logged_in}, カート: {cart_items}")

def delete_session_data(request):
    if 'user_id' in request.session:
        del request.session['user_id'] # 特定のキーのデータを削除
    request.session.clear() # 全てのSessionデータをクリア
    return HttpResponse("Sessionデータを削除しました。")

タグ: Django Python Web開発 ルーティング テンプレート

11月4日 01:21 投稿