Django基礎 #3 Models と ORM 基礎
#2 プロジェクトのセットアップ で空の blog アプリを作りました。今回はその中の models.py に DB の形 を描き、ORM でデータを扱う最初の一歩をつかみます。SQL を 1 行も直接書きません — それが ORM の約束です。
Model の一行定義 #
Django の Model は DB テーブルにマッピングされる Python クラス です。クラス 1 つ = テーブル 1 つ、クラスの属性 = カラム。
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_published = models.BooleanField(default=False)
def __str__(self) -> str:
return self.titleこのクラス 1 つで次の SQL が生成されます (直接書かなくていいです — マイグレーションが作ります)。
CREATE TABLE "blog_post" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(200) NOT NULL,
"content" text NOT NULL,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL,
"is_published" bool NOT NULL
);テーブル名は <app>_<モデル> (小文字) — blog_post。id は自動生成される PK です。
よく使うフィールドタイプ #
from django.db import models
class FieldDemo(models.Model):
# 文字列
name = models.CharField(max_length=100) # VARCHAR
description = models.TextField() # TEXT (長さ制限なし)
slug = models.SlugField(unique=True) # URL フレンドリーな文字列
# 数値
views = models.IntegerField(default=0)
price = models.DecimalField(max_digits=10, decimal_places=2)
rating = models.FloatField()
# ブール
is_active = models.BooleanField(default=True)
# 日付/時刻
published_at = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True) # 生成時に自動
updated_at = models.DateTimeField(auto_now=True) # 保存ごとに自動
# その他
email = models.EmailField()
website = models.URLField(blank=True)
image = models.ImageField(upload_to="images/", blank=True)
file = models.FileField(upload_to="files/", blank=True)
uuid = models.UUIDField()よく見られるオプション:
null=True— DB レベルでNULLを許可blank=True— フォーム検証で空の値を許可default=...— デフォルト値unique=True— 一意制約choices=[...]— 決まった値だけ許可 (Enum に近い)db_index=True— インデックス
nullvsblank混乱しやすいポイント —nullは DB、blankは フォーム。文字列フィールドにはnull=Trueをあまり使いません (空文字""にしておくのがコンベンション)。
choices — Enum 風
#
class Post(models.Model):
class Status(models.TextChoices):
DRAFT = "draft", "草稿"
PUBLISHED = "published", "公開済み"
ARCHIVED = "archived", "保管済み"
title = models.CharField(max_length=200)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.DRAFT,
)TextChoices (または IntegerChoices) が Python 3 の Enum のように動作しながら、DB 値 + 人が読むラベル を一度に定義します。フォーム / Admin で自動的に select オプションになります。
Migration — モデル変更を SQL に #
モデルを作ったり変えたりしたら 2 ステップ:
uv run python manage.py makemigrations blog
uv run python manage.py migrate各ステップの仕事:
makemigrations— モデルと現在のマイグレーション状態を比較し、差分を マイグレーションファイル として保存 (blog/migrations/0001_initial.pyのような)migrate— マイグレーションファイルを 実際の DB に適用
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Post",
fields=[
("id", models.AutoField(primary_key=True)),
("title", models.CharField(max_length=200)),
("content", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("is_published", models.BooleanField(default=False)),
],
),
]マイグレーションファイルは 必ず git にコミット します。チームメンバーが同じスキーマを再現できる必要があります。
よく使うマイグレーションコマンド #
# 適用状態の確認
uv run python manage.py showmigrations
# 特定のマイグレーションの SQL プレビュー
uv run python manage.py sqlmigrate blog 0001
# 巻き戻し (例: 0001 以前に)
uv run python manage.py migrate blog zero関係 — ForeignKey、ManyToMany、OneToOne #
ブログのドメインによくある関係たち:
from django.conf import settings
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def __str__(self) -> str:
return self.name
class Post(models.Model):
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # ユーザーモデル
on_delete=models.CASCADE,
related_name="posts",
)
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
bio = models.TextField(blank=True)3 種類の関係:
ForeignKey— N:1。Post -> User(1 人のユーザーが複数の記事)ManyToManyField— N:N。Post <-> Tag(1 つの記事に複数のタグ、1 つのタグに複数の記事)OneToOneField— 1:1。Profile <-> User
on_delete オプション
#
ForeignKey は 親が削除されたとき子をどうするか を必ず指定する必要があります。
| オプション | 動作 |
|---|---|
CASCADE | 親削除時に子も削除 (もっとも一般的) |
PROTECT | 子があれば親の削除をブロック |
SET_NULL | 子の FK を NULL に (null=True が必要) |
SET_DEFAULT | デフォルト値に (default= が必要) |
DO_NOTHING | DB に任せる (通常は使わない) |
related_name — 逆参照の名前
#
posts = user.posts.all() # related_name="posts"
comments = post.comments.all() # related_name="comments"related_name を与えないと post_set、comment_set のようなデフォルト名になります。明示的に与えるほうが可読性が上がります。
settings.AUTH_USER_MODEL を使う理由
#
from django.contrib.auth.models import User を直接 import しても動作はしますが、後でカスタム User モデルに変えるとき、すべての import を直さなければなりません。settings.AUTH_USER_MODEL にしておくと一度に差し替えられます。コンベンション です。
QuerySet — Django ORM の核心 #
モデルが作られたら、いよいよ ORM でデータを扱います。QuerySet は DB クエリを表現する Python オブジェクト — lazy に動作します (実際の呼び出し前まで SQL は出ません)。
uv run python manage.py shellCreate — 作る #
from blog.models import Post, Tag
from django.contrib.auth import get_user_model
User = get_user_model()
author = User.objects.create_user(username="curtis", password="secret123")
# 方法 1: インスタンス → save
post = Post(author=author, title="最初の記事", content="こんにちは!")
post.save()
# 方法 2: objects.create (一発)
post = Post.objects.create(author=author, title="2番目の記事", content="...")
# Tag M2M 追加
tag1 = Tag.objects.create(name="django", slug="django")
tag2 = Tag.objects.create(name="python", slug="python")
post.tags.add(tag1, tag2)Read — 取得 #
# 全件
Post.objects.all() # <QuerySet [<Post: 最初の記事>, ...]>
# PK で 1 件 (なければ DoesNotExist)
Post.objects.get(pk=1)
Post.objects.get(id=1)
# 条件
Post.objects.filter(is_published=True)
Post.objects.exclude(is_published=False)
# 最初 / 最後 / カウント
Post.objects.first()
Post.objects.last()
Post.objects.count()
# ソート
Post.objects.order_by("-created_at") # 新しい順
Post.objects.order_by("title") # 五十音順
# スライシング (LIMIT/OFFSET)
Post.objects.all()[:10] # 最初の 10 件
Post.objects.all()[10:20] # 次の 10 件
# 存在確認 (もっとも速い)
Post.objects.filter(author=author).exists()Lookup — __ で条件を作る
#
# テキスト
Post.objects.filter(title__icontains="django") # 大文字小文字無視の部分一致
Post.objects.filter(title__startswith="こんにちは")
Post.objects.filter(title__endswith="!")
# 比較
Post.objects.filter(views__gt=100) # > 100
Post.objects.filter(views__gte=100) # >= 100
Post.objects.filter(views__lt=10) # < 10
Post.objects.filter(views__lte=10) # <= 10
# 包含
Post.objects.filter(id__in=[1, 2, 3])
Post.objects.filter(id__range=(1, 100))
# NULL
Post.objects.filter(published_at__isnull=True)
# 日付
Post.objects.filter(created_at__year=2026)
Post.objects.filter(created_at__date="2026-04-14")
Post.objects.filter(created_at__gte="2026-01-01")
# 関係を辿る (FK のドット線)
Post.objects.filter(author__username="curtis")
Post.objects.filter(tags__name="django")__ (二重アンダースコア) が Django ORM の 共通文法 です。フィールド + lookup、または関係 + フィールド + lookup の形式。
Q オブジェクト — OR / 複雑な条件 #
filter(...) のキーワード引数はすべて AND です。OR や複雑な条件は Q で:
from django.db.models import Q
# OR
Post.objects.filter(Q(title__icontains="django") | Q(content__icontains="django"))
# AND + OR の混合
Post.objects.filter(
Q(is_published=True) & (Q(title__icontains="django") | Q(tags__name="django"))
)
# NOT
Post.objects.filter(~Q(author__username="curtis"))Update / Delete #
# 単件修正
post = Post.objects.get(pk=1)
post.title = "修正されたタイトル"
post.save()
# 一括修正 (SQL UPDATE 1 回)
Post.objects.filter(is_published=False).update(is_published=True)
# 単件削除
post.delete()
# 一括削除
Post.objects.filter(created_at__lt="2020-01-01").delete()update(...) は save() を経由しません — auto_now、signals のようなものが動きません。トレードオフを知って使ってください。詳しくは 中級 #2。
Lazy QuerySet — クエリはいつ出るのか #
qs = Post.objects.filter(is_published=True) # まだ SQL は出ない
qs = qs.order_by("-created_at") # まだ出ない
qs = qs[:10] # まだ出ない
for post in qs: # ← ここで SQL が 1 回出る
print(post.title)QuerySet は イテレーション、スライシング、len()、bool()、list()、count() など が呼ばれたときに初めて SQL を発射します。だからメソッドチェーンを自由に重ねられます。これは ORM 最適化の出発点です — 上級 #3 で詳しく。
Meta — モデル設定 #
class Post(models.Model):
title = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"] # デフォルトのソート
verbose_name = "記事" # Admin の表示名
verbose_name_plural = "記事一覧"
indexes = [
models.Index(fields=["-created_at"]),
]
constraints = [
models.UniqueConstraint(
fields=["author", "title"],
name="unique_author_title",
),
]Meta がモデルの DB / 表示関連の設定 を集めます。ordering を一度設定しておくと .all() に自動で適用されます。
まとめ #
今回つかんだもの:
- Model = Python クラス = DB テーブル、属性 = カラム
- フィールド —
CharField、TextField、DateTimeField、BooleanField、SlugField、… null(DB) とblank(フォーム) は別物TextChoicesで Enum 風makemigrations→migrateの 2 ステップ、マイグレーションファイルは git コミット- 関係 —
ForeignKey(N:1)、ManyToManyField(N:N)、OneToOneField(1:1) on_deleteは ForeignKey の必須、通常はCASCADErelated_nameで逆参照名を明示settings.AUTH_USER_MODELのコンベンション- QuerySet は lazy — イテレーション / スライシング /
exists()時に SQL を発射 - lookup
__icontains、__gt、__in、__year、__isnull、… Qオブジェクトで OR / NOTobjects.create、save、update()、delete()Metaで ordering / index / constraint
次回(#4 URL と Views)ではこのモデルで作ったデータを URL と view 関数 で公開し、render で最初の HTML レスポンスを作ってみます。ORM のより深い話 (select_related、prefetch_related、annotate) は 中級 #2 ORM 中級 で続きます。