Поля, типы данных, миграции, save(), delete(), __str__
Модели — это единственный источник правды о структуре данных вашего приложения. Django ORM превращает классы Python в таблицы базы данных, предоставляя высокоуровневый API для работы с данными.
💡 Правило: Каждая модель должна представлять одну сущность предметной области. Если модель делает больше одного — это нарушение SRP (Single Responsibility Principle).
from django.db import models
from django.utils import timezone
class Post(models.Model):
"""Модель поста в блоге."""
# Поля
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_published = models.BooleanField(default=False)
# Мета-класс
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['is_published', '-created_at']),
]
verbose_name = 'Пост'
verbose_name_plural = 'Посты'
# Методы
def __str__(self):
return self.title
def publish(self):
self.is_published = True
self.save(update_fields=['is_published', 'updated_at'])
@property
def is_recent(self):
return self.created_at > timezone.now() - timezone.timedelta(days=7)| Поле | Описание | Пример |
|---|---|---|
CharField(max_length=N) | Строка с ограничением длины | title = models.CharField(max_length=200) |
TextField | Большой текст без ограничений | content = models.TextField() |
SlugField | URL-friendly строка (латиница, цифры, дефис) | slug = models.SlugField(unique=True) |
EmailField | Email с валидацией | email = models.EmailField() |
URLField | URL с валидацией | website = models.URLField() |
| Поле | Описание | Пример |
|---|---|---|
IntegerField | Целое число | views = models.IntegerField(default=0) |
DecimalField(max_digits, decimal_places) | Десятичное число с фиксированной точностью | price = models.DecimalField(max_digits=10, decimal_places=2) |
FloatField | Число с плавающей точкой | rating = models.FloatField() |
| Поле | Описание | Пример |
|---|---|---|
DateField | Дата | birth_date = models.DateField() |
TimeField | Время | start_time = models.TimeField() |
DateTimeField | Дата и время | created_at = models.DateTimeField(auto_now_add=True) |
| Поле | Описание | Пример |
|---|---|---|
BooleanField | True/False | is_active = models.BooleanField(default=True) |
NullBooleanField | True/False/None (устарело, используйте BooleanField(null=True)) | maybe = models.BooleanField(null=True) |
JSONField | JSON-данные (PostgreSQL, MySQL 5.7+, SQLite 3.9+) | metadata = models.JSONField(default=dict) |
FileField | Файл | avatar = models.FileField(upload_to='avatars/') |
ImageField | Изображение (требует Pillow) | photo = models.ImageField(upload_to='photos/') |
# null=True — разрешает NULL в базе данных
# blank=True — разрешает пустое значение в формах/валидации
class Example(models.Model):
# Пустая строка вместо NULL (рекомендуется для строк)
name = models.CharField(max_length=100, blank=True)
# NULL в базе данных (для чисел, дат)
optional_count = models.IntegerField(null=True, blank=True)
# Обязательное поле
required_field = models.CharField(max_length=100) # blank=False по умолчанию⚠️ Важно: Избегайте
null=TrueдляCharField. Используйте пустую строку''вместо NULL.
# Статическое значение
status = models.CharField(max_length=20, default='draft')
# Вызываемое значение (функция без скобок!)
from uuid import uuid4
def generate_uuid():
return str(uuid4())
id = models.CharField(max_length=36, default=generate_uuid, primary_key=True)
# ОШИБКА: default=uuid4() вызовет функцию один раз при старте!# Уникальное значение в базе данных
email = models.EmailField(unique=True) # Никаких дубликатов
# Уникальность по нескольким полям
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'slug'], name='unique_user_slug')
]class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Черновик'),
('published', 'Опубликован'),
('archived', 'В архиве'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft'
)
# Использование:
# post.status = 'published'
# post.get_status_display() → 'Опубликован'class Author(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
author = models.ForeignKey(
Author,
on_delete=models.CASCADE, # Что делать при удалении автора
related_name='posts', # Имя для обратного доступа
null=True, # Разрешить NULL
blank=True # Разрешить пустое в формах
)
# Использование:
# post.author — получить автора поста
# author.posts.all() — все посты автора (через related_name)| Опция | Описание |
|---|---|
CASCADE | Удалить связанные объекты при удалении родителя |
PROTECT | Запретить удаление родителя (ProtectedError) |
SET_NULL | Установить NULL (требуется null=True) |
SET_DEFAULT | Установить значение по умолчанию |
SET() | Установить конкретное значение или вызвать функцию |
DO_NOTHING | Ничего не делать (может привести к integrity error) |
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Post(models.Model):
tags = models.ManyToManyField(
Tag,
related_name='posts',
blank=True # Разрешить пост без тегов
)
# Использование:
# post.tags.all() — все теги поста
# tag.posts.all() — все посты с тегом
# post.tags.add(tag1, tag2) — добавить теги
# post.tags.remove(tag1) — удалить тег
# post.tags.clear() — удалить все тегиclass Subscription(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
subscribed_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = ['user', 'channel'] # Одна подписка на канал
class Channel(models.Model):
name = models.CharField(max_length=100)
subscribers = models.ManyToManyField(
User,
through='Subscription', # Промежуточная модель
through_fields=('channel', 'user')
)class User(models.Model):
username = models.CharField(max_length=100, unique=True)
class Profile(models.Model):
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='profile'
)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', null=True)
# Использование:
# user.profile — профиль пользователя
# profile.user — пользователь профиляMeta класс определяет метаданные модели:
class Post(models.Model):
title = models.CharField(max_length=200)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# Порядок по умолчанию
ordering = ['-created_at', 'title']
# Индексы
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['title', 'created_at']),
models.Index(fields=['-created_at'], name='post_created_desc_idx'),
]
# Ограничения
constraints = [
models.UniqueConstraint(fields=['slug'], name='unique_slug'),
models.CheckConstraint(check=models.Q(price__gte=0), name='price_positive'),
]
# Человекочитаемые названия
verbose_name = 'Пост'
verbose_name_plural = 'Посты'
# Имя таблицы (если не нравится автогенерируемое)
db_table = 'blog_posts'
# Требование уникальности комбинации полей
unique_together = ['author', 'slug'] # Устарело, используйте UniqueConstraintclass Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
views = models.IntegerField(default=0)
def save(self, *args, **kwargs):
# Автогенерация slug при создании
if not self.slug and self.title:
from django.utils.text import slugify
self.slug = slugify(self.title)
# Вызов родительского save()
super().save(*args, **kwargs)
# Оптимизация: обновление только конкретных полей
def increment_views(self):
self.views += 1
self.save(update_fields=['views']) # Только это поле обновится в БДclass Post(models.Model):
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
def delete(self, using=None, keep_parents=False):
# Soft delete вместо физического удаления
self.is_deleted = True
self.deleted_at = timezone.now()
self.save(update_fields=['is_deleted', 'deleted_at'])
def hard_delete(self):
# Физическое удаление
super().delete()class Post(models.Model):
title = models.CharField(max_length=200)
def __str__(self):
return self.title # Отображается в админке и shell
class User(models.Model):
username = models.CharField(max_length=100)
email = models.EmailField()
def __str__(self):
return f'{self.username} ({self.email})'from django.urls import reverse
class Post(models.Model):
slug = models.SlugField(unique=True)
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.slug})
# Использование в шаблонах:
# <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>Создадим полную модель для блога:
# blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
from django.utils.text import slugify
from django.urls import reverse
User = get_user_model()
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True, blank=True)
description = models.TextField(blank=True)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children'
)
class Meta:
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('category_detail', kwargs={'slug': self.slug})
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True, blank=True)
class Meta:
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Черновик'),
('published', 'Опубликован'),
('archived', 'В архиве'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
excerpt = models.TextField(blank=True, help_text='Краткое описание')
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='posts'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='posts'
)
tags = models.ManyToManyField(
Tag,
blank=True,
related_name='posts'
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft'
)
views = models.PositiveIntegerField(default=0)
is_featured = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-published_at', '-created_at']
indexes = [
models.Index(fields=['-published_at']),
models.Index(fields=['status', '-published_at']),
models.Index(fields=['author', '-created_at']),
]
constraints = [
models.UniqueConstraint(fields=['slug'], name='unique_post_slug'),
]
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.slug})
def publish(self):
from django.utils import timezone
self.status = 'published'
self.published_at = timezone.now()
self.save(update_fields=['status', 'published_at', 'updated_at'])
@property
def is_recent(self):
from django.utils import timezone
return self.created_at > timezone.now() - timezone.timedelta(days=7)
class Comment(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='comments'
)
content = models.TextField()
is_approved = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['post', '-created_at']),
]
def __str__(self):
return f'Comment by {self.author} on {self.post}'Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.