はじめに
前章でトップページのデザインが完成しました。ただ表示していたのはコードに直接書いたダミーデータです。
この章ではモデルを作ってMySQLにデータを保存し、管理画面から記事を管理できるようにします。
この章でやること:
- モデル(Category・Post・DisplaySlot)を作る
- マイグレーションでDBにテーブルを作る
- 管理画面に登録する
- fixtureでテストデータを投入する
- views.pyをDBから取得するコードに差し替える
ついにDBデビューだね!ここを越えるとブログっぽさが一気に出るよ〜
モデルとは何か
モデルはDjangoとデータベースをつなぐ「設計図」です。
Pythonのクラスとして書くと、Djangoが自動でSQLに変換してMySQLにテーブルを作ってくれます。SQLを直接書く必要はありません。
# こう書くと...
class Post(models.Model):
title = models.CharField(max_length=200)
# MySQLにこんなテーブルが作られる
# CREATE TABLE blog_post (
# id INT AUTO_INCREMENT PRIMARY KEY,
# title VARCHAR(200)
# );
DBの設計
今回作るテーブルは3つです。
Categoryテーブル(カテゴリ)
| フィールド | 型 | 内容 |
|---|---|---|
| name | CharField | カテゴリ名(Django・Docker等) |
| slug | SlugField | URL用(django・docker等) |
Postテーブル(記事)
| フィールド | 型 | 内容 |
|---|---|---|
| title | CharField | タイトル |
| slug | SlugField | URL用(記事ごとに固有) |
| summary | CharField | 一覧用の要約 |
| body | TextField | 本文(Markdown) |
| category | ForeignKey | カテゴリ(Categoryと紐付け) |
| is_published | BooleanField | 公開フラグ |
| view_count | PositiveIntegerField | 閲覧数 |
| published_at | DateTimeField | 公開日時 |
| created_at | DateTimeField | 作成日時(自動) |
| updated_at | DateTimeField | 更新日時(自動) |
DisplaySlotテーブル(表示スロット)
| フィールド | 型 | 内容 |
|---|---|---|
| slot_type | CharField | スロット種別(特集・ピックアップ) |
| post | ForeignKey | 表示する記事 |
| order | IntegerField | 表示順 |
なぜDisplaySlotを別テーブルにするのか
「特集にどの記事を出すか」を管理する方法は2つあります:
| 方法 | 内容 |
|---|---|
| フラグ方式 | Postに is_featured フィールドを追加する |
| 別テーブル方式 | DisplaySlotテーブルで管理する |
フラグ方式はシンプルですが、「どのスロットに何件出すか」をコードで固定する必要があります。別テーブル方式なら管理画面からスロットの内容を自由に変えられます。変更に強い設計です。
迷ったら別テーブル!って覚えておいて。後から「やっぱ3個並べたい」ってなっても怖くないよ
Step1. models.pyを書く
~/myblog/blog/models.py を開いて以下に書き換えます。この章では文字データだけを扱い、アイキャッチ画像は第6章で追加します。
from django.db import models
from django.urls import reverse
class Category(models.Model):
"""記事のカテゴリ。"""
name = models.CharField(max_length=100, verbose_name='カテゴリ名')
# URLに使うため半角英数字・ハイフンのみ。uniqueで重複を防ぐ
slug = models.SlugField(unique=True, verbose_name='スラッグ')
class Meta:
verbose_name = 'カテゴリ'
verbose_name_plural = 'カテゴリ'
def __str__(self):
return self.name
class Post(models.Model):
"""ブログ記事。"""
title = models.CharField(max_length=200, verbose_name='タイトル')
# URL に使う識別子。例: /posts/django-tutorial/
# SEOと可読性のため記事ごとに固有の英数字を必ず設定する
slug = models.SlugField(max_length=200, unique=True, verbose_name='URL用スラッグ')
# 一覧ページ用の短い説明。省略可能
summary = models.CharField(max_length=300, verbose_name='要約', blank=True)
# 本文(Markdown形式)。管理画面では後の章で Toast UI Editor に置き換える
body = models.TextField(verbose_name='本文')
# カテゴリを削除しても記事は残す(SET_NULL)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name='カテゴリ',
)
is_published = models.BooleanField(default=False, verbose_name='公開')
# 未公開の場合はNullを許容する
published_at = models.DateTimeField(null=True, blank=True, verbose_name='公開日時')
# 記事詳細ページを開くたびに1加算する
view_count = models.PositiveIntegerField(default=0, verbose_name='閲覧数')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='作成日時')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新日時')
class Meta:
verbose_name = '記事'
verbose_name_plural = '記事'
# 公開日時の新しい順に並べる。
# 同じ published_at の記事が複数あっても順序がブレないよう、第2キーで id 降順を必ず併用する
ordering = ['-published_at', '-id']
def __str__(self):
return self.title
def get_absolute_url(self):
"""この記事の詳細ページURLを返す。
sitemap.xml や管理画面の「サイトを表示」リンクで使われる。
"""
return reverse('blog:detail', kwargs={'slug': self.slug})
class DisplaySlot(models.Model):
"""トップページの表示スロット管理。
どのスロットにどの記事を置くかを管理する。
スロットの種類や数をコードを変えずに管理画面から追加・変更できる。
"""
SLOT_FEATURED = 'featured'
SLOT_PICKUP = 'pickup'
SLOT_CHOICES = [
(SLOT_FEATURED, '特集バナー'),
(SLOT_PICKUP, 'ピックアップ'),
]
slot_type = models.CharField(max_length=50, choices=SLOT_CHOICES, verbose_name='スロット')
post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name='記事')
# 同じスロット内での並び順。数字が小さいほど先に表示する
order = models.IntegerField(default=0, verbose_name='表示順')
class Meta:
verbose_name = '表示スロット'
verbose_name_plural = '表示スロット'
ordering = ['slot_type', 'order']
def __str__(self):
return f'{self.get_slot_type_display()} / {self.post.title}'
get_absolute_url 忘れがちだけど絶対に書いておいて!sitemap.xml と管理画面の「サイトを表示」がここを見るから、後で「あれ動かない」になるよ
Step2. マイグレーションを実行する
モデルを書いただけではDBにテーブルは作られません。マイグレーションが必要です。
# マイグレーションファイルを生成する(設計図を作る)
docker compose run --rm web python manage.py makemigrations
# マイグレーションを実行する(実際にテーブルを作る)
docker compose run --rm web python manage.py migrate
makemigrationsとmigrateの違い
| コマンド | 役割 |
|---|---|
makemigrations |
models.pyの変更を検知してマイグレーションファイルを生成する |
migrate |
マイグレーションファイルをもとにDBにテーブルを作る |
2段階になっている理由は、生成されたファイルをGitで管理してチームで共有するためです。
Step3. 管理画面に登録する
~/myblog/blog/admin.py を開いて以下に書き換えます:
from django.contrib import admin
from .models import Category, DisplaySlot, Post
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
"""カテゴリの管理画面設定。"""
list_display = ['name', 'slug']
# nameを入力すると自動でslugを生成する
prepopulated_fields = {'slug': ('name',)}
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
"""記事の管理画面設定。"""
list_display = ['title', 'category', 'is_published', 'view_count', 'published_at']
list_filter = ['is_published', 'category']
# 一覧画面から直接クリックで切り替えられる
list_editable = ['is_published']
search_fields = ['title', 'body']
# titleを入力すると自動でslugを生成する
prepopulated_fields = {'slug': ('title',)}
@admin.register(DisplaySlot)
class DisplaySlotAdmin(admin.ModelAdmin):
"""表示スロットの管理画面設定。"""
list_display = ['slot_type', 'post', 'order']
list_editable = ['order']
list_filter = ['slot_type']
ordering = ['slot_type', 'order']
なお、本文の編集を Markdown エディタ(Toast UI Editor)に置き換えたり、アイキャッチのプレビューを管理画面に出したりするカスタマイズは後の章で必ず追加します。まずは素のDjango管理画面で動く形を固めます。
Step4. スーパーユーザーを作る
管理画面にログインするためのユーザーを作ります。
docker compose run --rm -it web python manage.py createsuperuser
ユーザー名・メールアドレス・パスワードを聞かれるので入力します。
作成後は http://localhost/admin/ にアクセスしてログインできます。
Step5. fixtureでテストデータを投入する
管理画面から1件ずつ記事を作るのは大変です。fixtureはテストデータをJSONファイルに書いておき、コマンド1つで一括投入できる仕組みです。第3章でダミーとして書いた「特集バナー+記事一覧」を、ここで本物のデータにします。
まずfixture用のフォルダとファイルを作ります。
mkdir -p ~/myblog/blog/fixtures
~/myblog/blog/fixtures/initial_data.json を作成して、以下をそのまま貼り付けます。
[
{ "model": "blog.category", "pk": 1, "fields": { "name": "Django", "slug": "django" } },
{ "model": "blog.category", "pk": 2, "fields": { "name": "Docker", "slug": "docker" } },
{ "model": "blog.category", "pk": 3, "fields": { "name": "Python", "slug": "python" } },
{
"model": "blog.post",
"pk": 1,
"fields": {
"title": "AIと一緒に作るDjangoブログ入門",
"slug": "django-blog-intro",
"summary": "DockerとDjangoを使ってブログサイトをゼロから作る方法を解説します。",
"body": "## はじめに\n\nこの記事では **Docker** と **Django** を使ってブログサイトをゼロから作ります。",
"category": 1,
"is_published": true,
"view_count": 0,
"published_at": "2026-05-21T00:00:00Z",
"created_at": "2026-05-21T00:00:00Z",
"updated_at": "2026-05-21T00:00:00Z"
}
},
{
"model": "blog.post",
"pk": 2,
"fields": {
"title": "Docker入門:なぜDockerを使うのか",
"slug": "docker-intro",
"summary": "MacにPythonをインストールしなくていい理由と、Dockerの仕組みを解説します。",
"body": "Dockerは仮想の箱を作るツールです。Pythonが入った箱を用意するだけで動かせます。",
"category": 2,
"is_published": true,
"view_count": 0,
"published_at": "2026-05-20T00:00:00Z",
"created_at": "2026-05-20T00:00:00Z",
"updated_at": "2026-05-20T00:00:00Z"
}
},
{
"model": "blog.post",
"pk": 3,
"fields": {
"title": "Bootstrap5でデザインを整える",
"slug": "bootstrap5-design",
"summary": "CSSを書かなくてもBootstrap5を使えば綺麗なデザインが作れます。",
"body": "Bootstrap5はCSSのフレームワークです。クラスをHTMLに書くだけでレスポンシブなレイアウトが作れます。",
"category": 1,
"is_published": true,
"view_count": 0,
"published_at": "2026-05-19T00:00:00Z",
"created_at": "2026-05-19T00:00:00Z",
"updated_at": "2026-05-19T00:00:00Z"
}
},
{
"model": "blog.post",
"pk": 4,
"fields": {
"title": "settings.pyの設定を理解する",
"slug": "settings-py-guide",
"summary": "DEBUG・ALLOWED_HOSTS・DATABASESなど重要な設定項目を解説します。",
"body": "DjangoのDEBUG・ALLOWED_HOSTS・DATABASESなど、settings.pyの重要な設定項目を解説します。",
"category": 1,
"is_published": true,
"view_count": 0,
"published_at": "2026-05-18T00:00:00Z",
"created_at": "2026-05-18T00:00:00Z",
"updated_at": "2026-05-18T00:00:00Z"
}
},
{
"model": "blog.post",
"pk": 5,
"fields": {
"title": "環境変数(.env)とは何か",
"slug": "env-file-guide",
"summary": "パスワードをコードに直接書いてはいけない理由を解説します。",
"body": "パスワードをコードに直接書くとGitHubで全世界に公開されてしまいます。.envに書くのが安全です。",
"category": 3,
"is_published": false,
"view_count": 0,
"published_at": null,
"created_at": "2026-05-17T00:00:00Z",
"updated_at": "2026-05-17T00:00:00Z"
}
},
{ "model": "blog.displayslot", "pk": 1, "fields": { "slot_type": "featured", "post": 1, "order": 0 } },
{ "model": "blog.displayslot", "pk": 2, "fields": { "slot_type": "pickup", "post": 2, "order": 0 } },
{ "model": "blog.displayslot", "pk": 3, "fields": { "slot_type": "pickup", "post": 3, "order": 1 } }
]
このデータの読み方
| 部分 | 内容 |
|---|---|
blog.category ×3 |
Django / Docker / Python のカテゴリ |
blog.post ×5 |
記事5件(うち pk:5 は is_published: false =下書き。一覧に出ないことを後で確認できます) |
blog.displayslot ×3 |
featured(特集バナー)に記事1、pickup(注目記事)に記事2・3を割り当て |
pk(プライマリキー)はレコードのID。category や post の数字は、その pk を指す参照です(例: post の "category": 1 は「Djangoカテゴリ」)。
is_published: false の記事をわざと1件入れてるのがポイント。このあと Step6 のビューで filter(is_published=True) を使うから、「下書きは一覧に出ない」が本当に効いてるか確認できるよ!
投入コマンドを実行します。
docker compose run --rm web python manage.py loaddata initial_data
Installed 11 object(s) と出れば成功です。ブラウザで http://localhost を開くと、第3章でダミーだった特集バナーと記事一覧が、今度はDBのデータで表示されます。
fixtureの使いどころ
| 用途 | 説明 |
|---|---|
| 開発環境のリセット | DBを消してやり直すときに同じデータを復元できる |
| チーム開発 | 全員が同じテストデータで開発できる |
逆に、今のDBの中身をfixtureとして書き出すこともできます(バックアップやデータ更新に便利)。
docker compose run --rm web python manage.py dumpdata blog --indent 2 > blog/fixtures/initial_data.json
Step6. views.pyをDBから取得するコードに書き換える
~/myblog/blog/views.py を開いて以下に書き換えます:
from django.shortcuts import render
from .models import Category, DisplaySlot, Post
def index(request):
"""トップページ(記事一覧)を表示する。"""
# 公開済みの記事を新しい順に取得する(モデルの ordering と揃える)
published = Post.objects.filter(is_published=True).order_by('-published_at', '-id')
# 特集バナー:管理画面で選ばれたスロットの先頭1件
# スロットが未設定の場合は最新の公開記事を代わりに表示する
# post__is_published=True で「下書きをスロットに入れた時にトップへ漏れる」のを防ぐ
featured_slot = (
DisplaySlot.objects
.filter(slot_type=DisplaySlot.SLOT_FEATURED, post__is_published=True)
.select_related('post')
.first()
)
featured = featured_slot.post if featured_slot else published.first()
# ピックアップ:管理画面で選ばれた記事を表示順に並べる(下書きは除外)
pickup = (
DisplaySlot.objects
.filter(slot_type=DisplaySlot.SLOT_PICKUP, post__is_published=True)
.select_related('post')
.order_by('order')
)
pickup_posts = [slot.post for slot in pickup]
# 記事一覧:特集・ピックアップを除いた公開記事を新しい順に表示する
exclude_pks = [p.pk for p in pickup_posts]
if featured:
exclude_pks.append(featured.pk)
post_list = published.exclude(pk__in=exclude_pks)
# サイドバー:よく読まれている記事(閲覧数の多い順で3件)
popular_posts = published.order_by('-view_count')[:3]
# サイドバー:カテゴリ一覧
categories = Category.objects.all()
context = {
'featured': featured,
'pickup': pickup_posts,
'post_list': post_list,
'popular_posts': popular_posts,
'categories': categories,
}
return render(request, 'blog/index.html', context)
テンプレートをDB版に合わせる
ビューが渡す変数(post_list)とモデルのフィールドに合わせて、~/myblog/blog/templates/blog/index.html の記事一覧部分を更新します。
{# 記事一覧(DBの公開記事。特集・ピックアップを除いた残り) #}
{% for post in post_list %}
<article class="blog-post mb-4">
<h2 class="display-5 mb-1">{{ post.title }}</h2>
<p class="blog-post-meta text-secondary">
{{ post.published_at|date:"Y年n月j日" }}
{% if post.category %}・{{ post.category.name }}{% endif %}
</p>
<p>{{ post.summary }}</p>
</article>
{% empty %}
<p>まだ記事がありません。</p>
{% endfor %}
{% empty %} は、ループする要素が0件のときに表示される構文です。記事が無いときに空白ではなく「まだ記事がありません」と出せます。
Step7. 起動確認
cd ~/myblog
docker compose up
ブラウザで http://localhost を開いてDBのデータが表示されれば成功です。
管理画面(http://localhost/admin/)から「表示スロット」を追加すると特集・ピックアップに任意の記事を表示できます:
- 「表示スロット」→「追加」
- スロット:
特集バナーまたはピックアップを選ぶ - 記事を選んで表示順を入力して保存
まとめ
この章でダミーデータからDBへの切り替えが完成しました。
管理画面で記事を登録
↓
DisplaySlotでトップページの表示を管理
↓
views.pyがDBから取得してテンプレートに渡す
↓
ブラウザに表示
次章では、記事タイトルをクリックして記事詳細ページを開けるようにし、カテゴリ絞り込みとキーワード検索まで作ります。
