Django models チートシート

cheatsheetPythonWeb開発

モデル定義の基本

モデルクラスを作成し、データベースのテーブル構造を定義します。

基本的なモデル

models.Model を継承してクラスを定義し、クラス変数としてフィールドを定義します。
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200, verbose_name="記事タイトル")
    content = models.TextField(verbose_name="記事内容", help_text="ここに記事の内容を入力します。")
    published_date = models.DateTimeField(auto_now_add=True, verbose_name="公開日")
    updated_date = models.DateTimeField(auto_now=True, verbose_name="更新日")
    is_public = models.BooleanField(default=True, verbose_name="公開フラグ")

    def __str__(self):
        return self.title

フィールドタイプ 📚

モデルで使用できる主要なフィールドタイプです。

フィールドタイプ 説明 主な引数
CharField 短い文字列(必須: max_length max_length, db_collation
TextField 長いテキスト文字列 db_collation
IntegerField 整数
PositiveIntegerField 正の整数
SmallIntegerField 小さい範囲の整数
BigAutoField 64ビット整数(自動インクリメント主キー用) primary_key=True
AutoField 32ビット整数(自動インクリメント主キー用、古いDjangoバージョンでのデフォルト) primary_key=True
FloatField 浮動小数点数
DecimalField 固定精度の10進数(必須: max_digits, decimal_places max_digits, decimal_places
BooleanField 真偽値 (True/False) default
NullBooleanField 真偽値 (True/False/Null) ※非推奨、BooleanField(null=True) を使用
DateField 日付 (YYYY-MM-DD) auto_now, auto_now_add
DateTimeField 日時 (YYYY-MM-DD HH:MM:SS) auto_now, auto_now_add
TimeField 時刻 (HH:MM:SS) auto_now, auto_now_add
DurationField 期間(Pythonのtimedelta
EmailField メールアドレス形式の文字列 max_length
URLField URL形式の文字列 max_length
UUIDField UUID default=uuid.uuid4, editable=False
FileField ファイルアップロード upload_to, storage
ImageField 画像ファイルアップロード(FileFieldを継承、Pillowが必要) upload_to, storage, height_field, width_field
JSONField JSON形式のデータを格納(一部DBのみネイティブ対応) encoder, decoder
GenericIPAddressField IPv4またはIPv6アドレス protocol, unpack_ipv4
BinaryField 生のバイナリデータ max_length

フィールドオプション ⚙️

フィールド定義時に指定できる共通のオプションです。

オプション 説明 デフォルト値
null データベースレベルでNULL値を許可するかどうか (True/False)。文字列ベースのフィールド(CharField, TextField)では非推奨。 False
blank フォームでの入力が空であることを許可するかどうか (True/False)。バリデーションに関係。 False
default フィールドのデフォルト値。値そのものか、呼び出し可能オブジェクトを指定。 指定なし
choices 選択肢を提供する。タプルのタプル (('DB値', '表示名'), ...) または Enum を指定。指定するとフォームでは通常セレクトボックスになる。 指定なし
verbose_name フィールドの人間可読な名前。Django Adminなどで表示される。 フィールド名をスペース区切りにしたもの
help_text フィールドの説明文。フォームやDjango Adminで表示される。 '' (空文字列)
primary_key このフィールドをモデルの主キーにするか (True/False)。 False
unique テーブル内でこのフィールドの値が一意である必要があるか (True/False)。 False
unique_for_date 指定された日付フィールド(DateField/DateTimeField)に対して、このフィールドの値が一意である必要があるか。フィールド名を文字列で指定。 指定なし
unique_for_month 指定された日付フィールドの「月」に対して、このフィールドの値が一意である必要があるか。 指定なし
unique_for_year 指定された日付フィールドの「年」に対して、このフィールドの値が一意である必要があるか。 指定なし
db_index このフィールドにデータベースインデックスを作成するか (True/False)。検索パフォーマンス向上のため。 False
db_column データベースで使用するカラム名を指定。省略するとフィールド名が使われる。 フィールド名
db_tablespace フィールドのインデックスで使用するデータベーステーブルスペース名を指定(DBが対応している場合)。 プロジェクト設定のDEFAULT_INDEX_TABLESPACE
editable Django Admin や ModelForm でこのフィールドが編集可能か (True/False)。Falseにすると表示されない。 True
error_messages デフォルトのエラーメッセージを上書きするための辞書。キーは null, blank, invalid, invalid_choice, unique, max_length など。 指定なし
validators フィールドに適用するバリデーター関数のリスト。 [] (空リスト)
auto_now (DateField/DateTimeField/TimeField) モデルが保存されるたびに、現在の日時に自動的に設定される (True/False)。編集不可(editable=False)かつ空不可(blank=False)になる。 False
auto_now_add (DateField/DateTimeField/TimeField) モデルが最初に作成されたときに、現在の日時に自動的に設定される (True/False)。編集不可(editable=False)かつ空不可(blank=False)になる。 False
upload_to (FileField/ImageField) ファイルのアップロード先ディレクトリ。文字列または関数を指定可能。日付フォーマット('%Y/%m/%d/')が利用できる。 指定なし (必須に近い)

リレーションシップ 🤝

モデル間の関連性を定義します。

ForeignKey (多対一)

他のモデルへの多対一(または一対一)の関連。on_delete 引数が必須。
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts') # Userモデルと多対一
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, related_name='posts') # Categoryモデルと多対一 (NULL許容)

    def __str__(self):
        return self.title

# on_delete オプション:
# models.CASCADE: 関連オブジェクトも一緒に削除
# models.PROTECT: 関連オブジェクトが存在する場合、削除を禁止 (ProtectedError)
# models.SET_NULL: 関連オブジェクト削除時、このフィールドをNULLに設定 (null=True が必要)
# models.SET_DEFAULT: 関連オブジェクト削除時、このフィールドをデフォルト値に設定 (default が必要)
# models.SET(): 関連オブジェクト削除時、指定した値または呼び出し可能オブジェクトの結果を設定
# models.DO_NOTHING: 何もしない (DBレベルでの制約違反の可能性あり)

related_name: 関連先のモデルから逆参照する際に使う名前。

limit_choices_to: フォームやAdminでの選択肢を制限する。辞書またはQオブジェクトを指定。

OneToOneField (一対一)

他のモデルへの一対一の関連。実質的には ForeignKey(unique=True) と同じ。on_delete が必須。
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) # Userと一対一、Profileの主キーにする
    bio = models.TextField(blank=True)
    location = models.CharField(max_length=100, blank=True)

    def __str__(self):
        return self.user.username

parent_link=True を指定すると、このフィールドがモデル継承のリンクを示すことを意味します。

ManyToManyField (多対多)

他のモデルへの多対多の関連。自動的に中間テーブルが作成される。
class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class Entry(models.Model):
    headline = models.CharField(max_length=255)
    tags = models.ManyToManyField(Tag, related_name='entries', blank=True) # Tagモデルと多対多

    def __str__(self):
        return self.headline

related_name: 逆参照時の名前。

through: 中間テーブルとして使用するカスタムモデルを指定。中間テーブルに追加フィールドを持たせたい場合に使用。

class Membership(models.Model):
    person = models.ForeignKey('Person', on_delete=models.CASCADE)
    group = models.ForeignKey('Group', on_delete=models.CASCADE)
    date_joined = models.DateField(auto_now_add=True)
    invite_reason = models.CharField(max_length=64, blank=True)

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership', related_name='groups') # Membershipを中間テーブルとして使用

    def __str__(self):
        return self.name

through_fields: カスタム中間テーブルを使用する際に、関連を特定するためのフィールド名をタプルで指定 ('source_field', 'target_field')。通常Djangoが推測できる場合は不要。

db_table: 中間テーブルの名前を明示的に指定する場合。

symmetrical: True (デフォルト) の場合、自分自身へのManyToManyFieldで、add() した際に逆の関係も自動的に作られる。False にすると非対称な関係(例: Twitterのフォロー)を表現できる。

Meta オプション 🔧

モデルクラス内に Meta クラスを定義して、モデルの挙動を設定します。

オプション 説明
db_table データベースで使用するテーブル名を明示的に指定。デフォルトは アプリ名_モデル名小文字
ordering モデルのデフォルトの並び順を指定。フィールド名のリストまたはタプル。'-fieldname' で降順。'?' でランダム。
verbose_name モデルの人間可読な単数名。Django Adminなどで使用。
verbose_name_plural モデルの人間可読な複数名。デフォルトは verbose_name + 's'
unique_together 複数のフィールドの組み合わせで一意制約を設定。タプルのタプル (('field1', 'field2'), ...)。※Django 4.1以降は非推奨、constraintsUniqueConstraint を使用。
index_together 複数のフィールドにまたがるインデックスを作成。タプルのタプル (('field1', 'field2'), ...)。※Django 4.1以降は非推奨、indexes を使用。
indexes データベースインデックスを定義。models.Index(fields=['field1', 'field2'], name='my_index') のリスト。
constraints データベース制約を定義。models.UniqueConstraint(fields=['field1', 'field2'], name='unique_fields')models.CheckConstraint(check=Q(age__gte=18), name='age_gte_18') のリスト。
get_latest_by Manager.latest()earliest() でデフォルトで使用するフィールド名(通常はDateField/DateTimeField)。
order_with_respect_to 指定したフィールド(通常はForeignKey)を基準にオブジェクトを並び替え可能にする。モデルに _order フィールドが追加され、get_RELATED_order(), set_RELATED_order() メソッドが追加される。
abstract True にすると、このモデルは抽象基底クラスとなり、データベーステーブルは作成されない。他のモデルに継承させて共通フィールドを定義するのに使う。
managed False にすると、Djangoのマイグレーションはこのモデルのテーブルを作成・変更・削除しない。既存のDBテーブルやビューに対してモデルを定義する場合などに使用。デフォルトは True
proxy True にすると、このモデルは他のモデルのプロキシとなる。元のモデルと同じテーブルを使用するが、Pythonレベルでの挙動(デフォルトマネージャ、メソッドなど)を変更できる。
app_label モデルが属するアプリケーションを明示的に指定。INSTALLED_APPS に含まれていないアプリのモデルを定義する場合などに使用。
db_tablespace モデルのテーブルが使用するデフォルトのデータベーステーブルスペース名を指定。
required_db_features このモデルが必要とするデータベース機能のリスト。指定された機能が接続中のDBでサポートされていない場合、マイグレーションなどでエラーが発生する。例: ['gis_enabled']
required_db_vendor このモデルが動作する特定のDBベンダーを指定 ('sqlite', 'postgresql', 'mysql', 'oracle')。指定外のDBではエラー。
from django.db import models
from django.db.models import Q, CheckConstraint, UniqueConstraint, Index

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField()
    added_date = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = 'product_catalog' # テーブル名を指定
        ordering = ['-added_date', 'name'] # 追加日の降順、次に名前の昇順で並び替え
        verbose_name = '商品'
        verbose_name_plural = '商品リスト'
        indexes = [
            models.Index(fields=['name']), # nameフィールドにインデックス
        ]
        constraints = [
            models.UniqueConstraint(fields=['name'], name='unique_product_name'), # nameフィールドに一意制約
            models.CheckConstraint(check=Q(price__gte=0), name='price_non_negative'), # priceが0以上である制約
        ]

データベース操作 (QuerySet API) 🔍

モデルマネージャ (objects) を通じてデータベースにアクセスします。

高度なクエリ 🧠

より複雑なデータベース問い合わせ方法です。

フィールドルックアップ (Field Lookups)

filter(), exclude(), get() などで使用する条件指定方法。フィールド名__ルックアップタイプ=値 の形式。
ルックアップ 説明
exact完全一致 (デフォルト、省略可)
iexact完全一致 (大文字小文字無視)
contains指定文字列を含む (部分一致)
icontains指定文字列を含む (大文字小文字無視)
inリストやQuerySet内のいずれかの値と一致
gtより大きい (greater than)
gte以上 (greater than or equal to)
ltより小さい (less than)
lte以下 (less than or equal to)
startswith指定文字列で始まる
istartswith指定文字列で始まる (大文字小文字無視)
endswith指定文字列で終わる
iendswith指定文字列で終わる (大文字小文字無視)
range範囲内 (タプルやリストで開始と終了を指定)
dateDateTimeField を日付で比較
year日付/日時の「年」で比較
month日付/日時の「月」で比較
day日付/日時の「日」で比較
week日付/日時の「年の何週目か」で比較
week_day日付/日時の「曜日」で比較 (1=日曜, 7=土曜 or DB依存)
quarter日付/日時の「四半期」で比較
timeDateTimeField を時刻で比較
hour日時/時刻の「時」で比較
minute日時/時刻の「分」で比較
second日時/時刻の「秒」で比較
isnull値が NULL かどうか (True/False を指定)
regex正規表現で検索
iregex正規表現で検索 (大文字小文字無視)
# 例
Product.objects.filter(name__icontains='app') # 名前に 'app' (大文字小文字問わず) を含む
Product.objects.filter(added_date__year=2024) # 2024年に追加された
Product.objects.filter(stock__in=[10, 20, 30]) # 在庫が10, 20, 30 のいずれか
Product.objects.filter(price__range=(1.00, 2.00)) # 価格が1.00から2.00の間

リレーションをまたいだルックアップ

ForeignKey や ManyToManyField を通じて関連先のモデルのフィールドで絞り込む。__ (ダブルアンダースコア) を使う。
# Post モデル (author は User への ForeignKey, category は Category への ForeignKey)
# 'admin' というユーザー名の著者が書いた投稿
Post.objects.filter(author__username='admin')

# 'News' という名前のカテゴリに属する投稿
Post.objects.filter(category__name='News')

# 'admin' というユーザー名の著者が書いた 'News' カテゴリの投稿
Post.objects.filter(author__username='admin', category__name='News')

# Entry モデル (tags は Tag への ManyToManyField)
# 'python' という名前のタグを持つエントリー
Entry.objects.filter(tags__name='python')

# 'python' または 'django' という名前のタグを持つエントリー
Entry.objects.filter(tags__name__in=['python', 'django']).distinct() # distinct() で重複を除く

# 逆参照 (User モデルから Post を絞り込む)
# 2024年以降に公開された投稿を持つユーザー
from django.contrib.auth.models import User
User.objects.filter(posts__published_date__year__gte=2024).distinct() # related_name='posts' を使用

Q オブジェクト (複雑な OR 条件)

複雑な条件 (特に OR 条件) を組み合わせるために使用。| (OR), & (AND), ~ (NOT) で組み合わせる。
from django.db.models import Q

# 名前が 'A' で始まる または 価格が 1.00 未満の商品
Product.objects.filter(Q(name__startswith='A') | Q(price__lt=1.00))

# 名前が 'A' で始まり かつ 価格が 1.00 未満の商品 (通常の filter と同じ)
Product.objects.filter(Q(name__startswith='A') & Q(price__lt=1.00))
# または
Product.objects.filter(name__startswith='A', price__lt=1.00)

# 名前が 'Apple' ではない商品
Product.objects.filter(~Q(name='Apple'))
# または
Product.objects.exclude(name='Apple')

# 複雑な組み合わせ: (名前が 'A' で始まる AND 在庫が 100 以上) OR (価格が 5.00 以上)
Product.objects.filter(
    (Q(name__startswith='A') & Q(stock__gte=100)) | Q(price__gte=5.00)
)

集計 (Aggregation)

QuerySet 全体に対する集計値を計算する。aggregate() メソッドを使用。結果は辞書。
from django.db.models import Avg, Max, Min, Sum, Count, StdDev, Variance

# 全商品の平均価格、最高価格、最低価格
aggregation_result = Product.objects.aggregate(
    avg_price=Avg('price'),
    max_price=Max('price'),
    min_price=Min('price')
)
print(aggregation_result) # {'avg_price': Decimal('...'), 'max_price': Decimal('...'), 'min_price': Decimal('...')}

# 商品の総数
total_products = Product.objects.aggregate(total=Count('id')) # または Count('*')
print(total_products) # {'total': ...}

# 在庫の合計
total_stock = Product.objects.aggregate(total_stock=Sum('stock'))
print(total_stock) # {'total_stock': ...}

# 価格の標準偏差と分散
price_stats = Product.objects.aggregate(price_stddev=StdDev('price'), price_variance=Variance('price'))
print(price_stats)

アノテーション (Annotation)

QuerySet の各オブジェクトに対して、集計値や計算結果などの新しいフィールドを追加する。annotate() メソッドを使用。
from django.db.models import Count, Avg

# 各カテゴリに属する投稿の数をアノテーションとして追加
categories = Category.objects.annotate(num_posts=Count('posts')) # related_name='posts'
for category in categories:
    print(f"{category.name}: {category.num_posts}")

# 各著者ごとの投稿の平均文字数 (TextFieldはAvgできないので注意、例として示す)
# User.objects.annotate(avg_post_length=Avg('posts__content_length_field')) # posts__ は User から Post への逆参照

# 各商品に、自分より価格が高い商品の数をアノテーション (サブクエリ使用例)
from django.db.models import OuterRef, Subquery, IntegerField
higher_priced_count_subquery = Product.objects.filter(price__gt=OuterRef('price')).values('id')
products_with_rank = Product.objects.annotate(
    num_more_expensive=Count(Subquery(higher_priced_count_subquery))
).order_by('-num_more_expensive')

for p in products_with_rank:
    print(f"{p.name}: {p.num_more_expensive} items are more expensive")

# F() オブジェクトと組み合わせる
# 各商品の価格に10%の税を加えた 'price_with_tax' をアノテーション
from django.db.models import F, DecimalField
from django.db.models.functions import Cast

products_with_tax = Product.objects.annotate(
    price_with_tax=Cast(F('price') * 1.1, output_field=DecimalField(max_digits=10, decimal_places=2))
)
for p in products_with_tax:
    print(f"{p.name}: Price={p.price}, Price with Tax={p.price_with_tax}")

QuerySet のスライシングと順序付け

# 順序付け (ordering)
products_by_name = Product.objects.order_by('name') # 名前昇順
products_by_price_desc = Product.objects.order_by('-price') # 価格降順
products_by_name_then_price = Product.objects.order_by('name', '-price') # 名前昇順、同じ名前なら価格降順

# スライシング (LIMIT と OFFSET)
first_five_products = Product.objects.all()[:5] # 最初の5件
products_from_6_to_10 = Product.objects.all()[5:10] # 6件目から10件目 (0ベース)

# 順序付けとスライシングの組み合わせ (よく使うパターン)
top_5_expensive_products = Product.objects.order_by('-price')[:5]

注意: order_by() なしでのスライシングは、データベースによっては順序が保証されない場合があります。

values()values_list()

特定のフィールドの値だけを辞書 (values) またはタプル (values_list) のリストとして取得する。メモリ効率が良い場合がある。
# 商品名と価格だけを辞書のリストとして取得
product_data = Product.objects.values('name', 'price')
# [{'name': 'Apple', 'price': Decimal('1.60')}, {'name': 'Banana', 'price': Decimal('0.80')}, ...]

# 商品名だけをタプルのリストとして取得
product_names_tuple = Product.objects.values_list('name')
# [('Apple',), ('Banana',), ...]

# flat=True で単一フィールドの値を直接リストで取得
product_names_list = Product.objects.values_list('name', flat=True)
# ['Apple', 'Banana', ...]

# アノテーションと組み合わせる
category_post_counts = Category.objects.annotate(num_posts=Count('posts')).values('name', 'num_posts')
# [{'name': 'News', 'num_posts': 15}, {'name': 'Tech', 'num_posts': 22}, ...]

select_related()prefetch_related() (パフォーマンス最適化)

N+1 問題を解決し、データベースクエリ回数を削減するための手法。
# N+1 問題の例 (ループ内で関連オブジェクトにアクセスするたびにクエリが発生)
posts = Post.objects.all()
for post in posts:
    print(post.title, post.author.username) # post ごとに User を取得するクエリが走る
    print(post.category.name)              # post ごとに Category を取得するクエリが走る

# select_related(): ForeignKey, OneToOneField (単一オブジェクト) を JOIN して取得
posts_optimized = Post.objects.select_related('author', 'category').all()
for post in posts_optimized:
    print(post.title, post.author.username) # JOIN済みなので追加クエリ不要
    print(post.category.name)              # JOIN済みなので追加クエリ不要

# prefetch_related(): ManyToManyField, 逆参照 ForeignKey/OneToOneField (複数オブジェクト) を別クエリでまとめて取得
entries = Entry.objects.prefetch_related('tags').all()
for entry in entries:
    print(entry.headline)
    # entry.tags.all() を実行しても、事前に取得したデータを使うため追加クエリ不要
    print([tag.name for tag in entry.tags.all()])

# ネストした関連も取得可能
users = User.objects.prefetch_related('posts__category').all() # User -> Posts -> Category
for user in users:
    print(user.username)
    for post in user.posts.all(): # posts は prefetch 済み
        print(f"  - {post.title} (Category: {post.category.name})") # category も prefetch 済み

select_related は SQL JOIN を使用。prefetch_related は Python 側で結合を行うための追加クエリ(通常は IN 句を使用)を発行。

defer()only()

取得するフィールドを制限し、不要なデータの読み込みを避ける。
# defer(): 指定したフィールドを遅延読み込み (アクセス時に追加クエリが発生)
# 大量のテキストデータなど、すぐには不要なフィールドを除外する
products = Product.objects.defer('description', 'specifications')
for p in products:
    print(p.name) # description や specifications へのアクセスがない限り、それらは読み込まれない
    # print(p.description) # ここで description を取得するための追加クエリが発生

# only(): 指定したフィールドのみを即時読み込み (他のフィールドは遅延読み込み)
products = Product.objects.only('name', 'price')
for p in products:
    print(p.name, p.price) # name と price は読み込み済み
    # print(p.stock) # ここで stock を取得するための追加クエリが発生

select_related と併用する場合、関連オブジェクトのフィールドも defer/only の対象に含めることができます (例: only('name', 'author__username'))。

生のSQL (Raw SQL)

ORMでは表現が難しい複雑なクエリや、パフォーマンスが重要な場合に生のSQLを実行する。
# Manager.raw(): モデルインスタンスを返す生のSQLクエリ
raw_products = Product.objects.raw('SELECT * FROM product_catalog WHERE price > %s', [1.00])
for p in raw_products:
    print(p.name, p.price)

# connection.cursor(): DB API (DB-API 2.0) を直接使用 (モデルを経由しない)
from django.db import connection

with connection.cursor() as cursor:
    cursor.execute("UPDATE product_catalog SET stock = %s WHERE name = %s", [50, 'Apple'])
    cursor.execute("SELECT COUNT(*) FROM product_catalog")
    row = cursor.fetchone()
    product_count = row[0]

print(f"Total products: {product_count}")

注意: 生のSQLはデータベースの種類に依存する可能性があり、SQLインジェクションのリスクがあるため、パラメータは必ずプレースホルダ (%s) を使用してください。

モデルマネージャ (Managers) 🧑‍💼

モデルのテーブルレベルの操作(主にクエリ)を提供するインターフェース。

デフォルトマネージャ (objects)

各モデルには少なくとも1つのマネージャが必要です。明示的に定義しない場合、objects = models.Manager() という名前のデフォルトマネージャが自動的に追加されます。
class MyModel(models.Model):
    # フィールド定義...
    pass # 自動的に objects = models.Manager() が追加される

# 利用
MyModel.objects.all()

カスタムマネージャ

共通のフィルタリング条件を持つメソッドや、テーブル全体に対する操作を追加するためにカスタムマネージャを定義します。
from django.db import models
from django.utils import timezone

# カスタム QuerySet (メソッドチェーンを可能にするため)
class PublishedQuerySet(models.QuerySet):
    def published_in_year(self, year):
        return self.filter(publish_date__year=year)

# カスタムマネージャ
class PublishedManager(models.Manager):
    def get_queryset(self):
        # カスタムQuerySetを使用し、デフォルトで公開済みのものだけを返すようにする
        return PublishedQuerySet(self.model, using=self._db).filter(status='published', publish_date__lte=timezone.now())

    # マネージャに直接メソッドを追加することも可能
    def get_published_count(self):
        return self.get_queryset().count()

    def create_published_article(self, title, content):
        # 公開状態で記事を作成するヘルパーメソッド
        article = self.create(title=title, content=content, status='published', publish_date=timezone.now())
        return article

class Article(models.Model):
    STATUS_CHOICES = (
        ('draft', '下書き'),
        ('published', '公開済み'),
    )
    title = models.CharField(max_length=200)
    content = models.TextField()
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    publish_date = models.DateTimeField(null=True, blank=True)

    # デフォルトマネージャをカスタムマネージャに置き換える
    objects = models.Manager() # 通常のマネージャも残しておくことが多い
    published = PublishedManager() # カスタムマネージャを追加

    def __str__(self):
        return self.title

# 利用例
# 公開済みの記事のみ取得
published_articles = Article.published.all()

# 2023年に公開された記事を取得 (カスタムQuerySetのメソッド)
articles_2023 = Article.published.published_in_year(2023)

# 公開済みの記事数を取得 (カスタムManagerのメソッド)
published_count = Article.published.get_published_count()

# 全ての記事を取得 (デフォルトマネージャを使用)
all_articles = Article.objects.all()

# 公開状態で記事を作成 (カスタムManagerのメソッド)
new_article = Article.published.create_published_article(title="新しい記事", content="本文...")

モデルに複数のマネージャを定義した場合、最初のマネージャ (コードで最初に定義されたもの) が Django によって「デフォルト」マネージャとして扱われます (例: Django Admin など)。Meta.default_manager_name で明示的にデフォルトを指定することも可能です。

モデルメソッド 🛠️

個々のモデルインスタンスに対するカスタム操作を定義します。

カスタムメソッドの追加

モデルクラス内に通常の Python メソッドを定義します。
import datetime
from django.db import models
from django.utils import timezone

class Poll(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        """
        最近 (1日以内) に公開されたかどうかを返す
        """
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

    # 管理サイトでの表示をカスタマイズするための属性
    was_published_recently.admin_order_field = 'pub_date' # ソート可能にする
    was_published_recently.boolean = True                 # アイコン表示にする
    was_published_recently.short_description = 'Published recently?' # 列ヘッダー

    def publish(self):
        """
        投票を公開状態にする (例: publish_dateを設定)
        """
        if self.pub_date is None:
            self.pub_date = timezone.now()
            self.save(update_fields=['pub_date'])

# 利用例
poll = Poll.objects.get(pk=1)
if poll.was_published_recently():
    print(f"'{poll.question_text}' は最近公開されました。")

# 投票を公開
poll.publish()

save() メソッドのオーバーライド

オブジェクトが保存される前後にカスタムロジックを実行する場合。
class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True, blank=True)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            # slug が空の場合、タイトルから自動生成する (例)
            from django.utils.text import slugify
            self.slug = slugify(self.title)[:200] # 200文字に制限

            # スラグが一意であることを確認 (簡易的な例)
            original_slug = self.slug
            counter = 1
            while BlogPost.objects.filter(slug=self.slug).exclude(pk=self.pk).exists():
                self.slug = f"{original_slug}-{counter}"
                counter += 1

        # 親クラスの save() を呼び出して実際の保存処理を行う
        super().save(*args, **kwargs)

    def __str__(self):
        return self.title

注意: save() をオーバーライドする際は、必ず super().save(*args, **kwargs) を呼び出すようにしてください。また、update()bulk_create() などの QuerySet メソッドでは、オーバーライドした save() は呼び出されません。

delete() メソッドのオーバーライド

オブジェクトが削除される前後にカスタムロジックを実行する場合。
class Document(models.Model):
    title = models.CharField(max_length=200)
    file = models.FileField(upload_to='documents/')

    def delete(self, *args, **kwargs):
        # データベースからレコードを削除する前に、関連するファイルを削除する
        self.file.delete(save=False) # save=False でモデルの再保存を防ぐ
        super().delete(*args, **kwargs) # 親クラスの delete() を呼び出してDBレコードを削除

    def __str__(self):
        return self.title

注意: delete() をオーバーライドする際は、必ず super().delete(*args, **kwargs) を呼び出すようにしてください。また、QuerySet の delete() メソッドでは、オーバーライドした delete() は呼び出されません。

get_absolute_url()

オブジェクトの詳細ページのURLを返す規約的なメソッド。Django Admin や他のコンポーネントで利用される。
from django.db import models
from django.urls import reverse

class Article(models.Model):
    # ... (title, slug, content など)
    slug = models.SlugField(unique=True)

    def get_absolute_url(self):
        # 'article-detail' という名前のURLパターンを逆引きする
        # kwargs={'slug': self.slug} でURLに必要なパラメータを渡す
        return reverse('article-detail', kwargs={'slug': self.slug})

# urls.py での対応するURLパターンの例
# from django.urls import path
# from . import views
# urlpatterns = [
#     path('articles/<slug:slug>/', views.ArticleDetailView.as_view(), name='article-detail'),
# ]

マイグレーション 🔄

モデルの変更をデータベーススキーマに反映させる仕組み。

マイグレーションファイルの作成

モデル定義を変更した後、その変更内容をマイグレーションファイルとして生成します。
python manage.py makemigrations [app_label]

[app_label] を省略すると、変更があった全てのアプリケーションのマイグレーションが作成されます。特定のアプリケーションのみを対象にする場合は、アプリ名を指定します。

生成されたファイル (例: yourapp/migrations/0002_add_new_field.py) には、スキーマ変更を適用するためのPythonコードが含まれます。

マイグレーションの適用

生成されたマイグレーションファイルをデータベースに適用し、実際のスキーマを変更します。
python manage.py migrate [app_label] [migration_name]

引数を省略すると、適用されていない全てのマイグレーションが全てのアプリケーションに対して実行されます。

[app_label] を指定すると、そのアプリケーションのマイグレーションのみが適用されます。

[app_label] [migration_name] を指定すると、そのアプリケーションの指定したマイグレーション(とその依存関係)まで適用またはロールバックされます。zero を指定すると、そのアプリの全てのマイグレーションがロールバックされます。

マイグレーションの状態確認

どのマイグレーションが適用済みかを確認します。
python manage.py showmigrations [app_label]

適用済みのマイグレーションには [X] が、未適用のものには [ ] が表示されます。

SQL の表示

マイグレーションが実行する実際のSQL文を確認します。データベースに適用する前に確認したい場合に便利です。
python manage.py sqlmigrate [app_label] [migration_name]

例: python manage.py sqlmigrate myapp 0002

カスタムマイグレーション (データマイグレーション)

スキーマ変更だけでなく、データの移行や変換を行うためのマイグレーション。
# 空のマイグレーションファイルを作成
python manage.py makemigrations --empty yourapp

生成された空のマイグレーションファイル (例: yourapp/migrations/0003_auto_xxxx.py) を編集し、migrations.RunPython()migrations.RunSQL() を使用してカスタム操作を記述します。

# Generated by Django X.X on YYYY-MM-DD HH:MM
from django.db import migrations

def combine_names(apps, schema_editor):
    # マイグレーション実行時点でのモデルを取得
    Person = apps.get_model('yourapp', 'Person')
    for person in Person.objects.all():
        # 既存の first_name と last_name を結合して full_name に設定
        person.full_name = f"{person.first_name} {person.last_name}"
        person.save(update_fields=['full_name'])

def split_names(apps, schema_editor):
    # ロールバック時の処理 (例)
    Person = apps.get_model('yourapp', 'Person')
    for person in Person.objects.all():
        parts = person.full_name.split(' ', 1)
        person.first_name = parts[0]
        person.last_name = parts[1] if len(parts) > 1 else ''
        person.save(update_fields=['first_name', 'last_name'])


class Migration(migrations.Migration):

    dependencies = [
        ('yourapp', '0002_add_fullname_field'), # 依存する前のマイグレーション
    ]

    operations = [
        # RunPython に forward 関数と reverse 関数 (ロールバック用) を渡す
        migrations.RunPython(combine_names, reverse_code=split_names),
    ]

コメント

タイトルとURLをコピーしました