← Назад к вопросам
Как организовать построение общей модели данных для проекта?
2.8 Senior🔥 191 комментариев
#Soft Skills#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Организация построения общей модели данных для проекта
Модель данных — основа всей системы. Правильное проектирование экономит месяцы работы на переделку. Рассмотрим систематический подход.
1. Фаза анализа требований
Сначала разберёмся, какие данные нужны:
# Методология Event Storming
# 1. Выделяем все события в системе:
# - User.registered
# - Post.created
# - Post.commented
# - Comment.liked
# - Post.deleted
# 2. Определяем команды (actions):
# - RegisterUser
# - CreatePost
# - AddComment
# - LikeComment
# - DeletePost
# 3. Определяем агрегаты (сущности):
class User: # Агрегат
id: UUID
username: str
email: str
created_at: datetime
posts: List[Post] # Отношение
class Post: # Агрегат
id: UUID
author_id: UUID
title: str
content: str
created_at: datetime
comments: List[Comment] # Отношение
class Comment: # Агрегат
id: UUID
post_id: UUID
author_id: UUID
content: str
created_at: datetime
likes_count: int
2. Entity-Relationship Diagram (ERD)
Визуализируем отношения между сущностями:
┌─────────────┐
│ User │
├─────────────┤
│ id (PK) │
│ username │
│ email │
│ created_at │
└────────┬────┘
│ 1:N
│
▼
┌────────────┐
│ Post │
├────────────┤
│ id (PK) │
│ user_id(FK)│
│ title │
│ content │
│ created_at │
└────────┬───┘
│ 1:N
│
▼
┌─────────────┐
│ Comment │
├─────────────┤
│ id (PK) │
│ post_id(FK) │
│ user_id(FK) │
│ content │
│ created_at │
└─────────────┘
3. Нормализация данных
Основные нормальные формы:
# 1NF: Атомарные значения (нет массивов/объектов в ячейке)
# ПЛОХО
class Post:
comments = ['Comment 1', 'Comment 2'] # Массив в одном поле
# ХОРОШО
class Post:
id: UUID
class Comment:
post_id: UUID # Отдельная таблица
content: str
# 2NF: Каждое неключевое поле зависит от всего первичного ключа
# ПЛОХО - student_name зависит только от student_id, не от course_id
class Enrollment:
student_id: UUID
course_id: UUID
student_name: str # Неправильно здесь
grade: str
# ХОРОШО - отделяем в Student
class Student:
id: UUID
name: str
class Enrollment:
student_id: UUID
course_id: UUID
grade: str
# 3NF: Нет транзитивных зависимостей
# ПЛОХО - city зависит от country, а не от student
class Student:
id: UUID
name: str
country_id: UUID
city: str # Лучше в Country
# ХОРОШО
class Country:
id: UUID
name: str
class City:
id: UUID
name: str
country_id: UUID
class Student:
id: UUID
name: str
city_id: UUID
4. Реализация на Django/SQLAlchemy
from django.db import models
from django.utils import timezone
import uuid
class User(models.Model):
"""Пользователь системы."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
username = models.CharField(max_length=100, unique=True)
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=100, blank=True)
last_name = models.CharField(max_length=100, blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users'
indexes = [
models.Index(fields=['email']),
models.Index(fields=['username']),
]
def __str__(self):
return self.username
class Post(models.Model):
"""Пост, созданный пользователем."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
title = models.CharField(max_length=200)
content = models.TextField()
is_published = 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:
db_table = 'posts'
indexes = [
models.Index(fields=['author', 'is_published']),
models.Index(fields=['created_at']),
]
ordering = ['-created_at']
def publish(self):
self.is_published = True
self.published_at = timezone.now()
self.save()
class Comment(models.Model):
"""Комментарий к посту."""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
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()
likes_count = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'comments'
indexes = [
models.Index(fields=['post', 'created_at']),
]
ordering = ['created_at']
5. Определение типов данных
# Правильный выбор типов — важен для производительности
class Product(models.Model):
# Строки
name = models.CharField(max_length=200) # Для коротких строк
description = models.TextField() # Для длинных текстов
# Числа
price = models.DecimalField(max_digits=10, decimal_places=2) # Цена (точность!)
quantity = models.IntegerField() # Целые числа
rating = models.FloatField() # Вещественные
# Даты
created_at = models.DateTimeField(auto_now_add=True) # Дата+время
created_date = models.DateField() # Только дата
available_from = models.TimeField() # Только время
# Специальные
is_available = models.BooleanField(default=True) # Boolean
image = models.ImageField(upload_to='products/') # Файлы
data = models.JSONField(default=dict) # JSON
status = models.CharField(
max_length=20,
choices=[('active', 'Активный'), ('inactive', 'Неактивный')]
)
6. Отношения между таблицами
# One-to-Many (1:N)
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
# Один Author может иметь много Book
# Book.author — один объект
# Author.books.all() — QuerySet
# Many-to-Many (N:N)
class Student(models.Model):
name = models.CharField(max_length=100)
class Course(models.Model):
title = models.CharField(max_length=200)
students = models.ManyToManyField(Student, related_name='courses')
# Один Student может быть в многих Course
# Один Course может иметь многих Student
# Student.courses.all() — QuerySet
# Course.students.all() — QuerySet
# Many-to-Many with extra data
class StudentCourse(models.Model):
"""Таблица связи с дополнительными полями."""
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
grade = models.CharField(max_length=2) # A, B, C...
enrolled_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('student', 'course') # Один студент - один курс
7. Миграции данных
# 1. Создаём начальную миграцию
# python manage.py makemigrations
# python manage.py migrate
# 2. При изменении модели
class User(models.Model):
# Добавили новое поле
phone_number = models.CharField(max_length=20, null=True, blank=True)
# python manage.py makemigrations
# Создастся файл с миграцией
# 3. Кастомная миграция для заполнения данных
from django.db import migrations
def populate_phone(apps, schema_editor):
User = apps.get_model('app', 'User')
for user in User.objects.all():
user.phone_number = '+7-900-000-00-00'
user.save()
class Migration(migrations.Migration):
dependencies = [('app', '0001_initial')]
operations = [
migrations.RunPython(populate_phone),
]
8. Документирование модели
class Order(models.Model):
"""
Заказ, созданный пользователем.
Поля:
id: Уникальный идентификатор (UUID)
user_id: Ссылка на пользователя, который создал заказ
total_amount: Общая сумма заказа (Decimal)
status: Статус заказа (pending, completed, cancelled)
created_at: Время создания (DateTimeField)
Отношения:
- One-to-Many с OrderItem через order_items
- Many-to-One с User через user
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
user = models.ForeignKey(User, on_delete=models.PROTECT, related_name='orders')
total_amount = models.DecimalField(max_digits=12, decimal_places=2)
status = models.CharField(
max_length=20,
choices=[
('pending', 'В ожидании'),
('completed', 'Завершён'),
('cancelled', 'Отменён'),
],
default='pending'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
9. Рефакторинг модели
# ПЛОХО: слишком много информации в одной модели
class User(models.Model):
name, email, phone, address, city, country, zip_code
company_name, company_address, tax_id
preferences, notifications, theme
# Смешаны персональные, адресные, компанейские, предпочтения данные
# ХОРОШО: разделяем на несколько моделей
class User(models.Model):
name, email, phone
class Address(models.Model):
user = ForeignKey(User)
address, city, country, zip_code
class Company(models.Model):
user = OneToOneField(User)
name, address, tax_id
class UserPreferences(models.Model):
user = OneToOneField(User)
notifications, theme
10. Лучшие практики
- Начните с анализа требований — не проектируйте без понимания домена
- Используйте Event Storming — определите все события и команды
- Нормализуйте данные — избегайте дублирования
- Выбирайте правильные типы данных — DECIMAL для денег, не FLOAT
- Добавляйте индексы на часто используемые поля — WHERE, JOIN, ORDER BY
- Документируйте модель — docstring, комментарии, диаграммы
- Используйте constraints — unique, check, foreign key с правильным on_delete
- Тестируйте отношения — убедитесь что каскадное удаление работает
- Планируйте масштабирование — учтите будущий рост данных
- Версионируйте схему — миграции — источник истины
Правильная модель данных — основа успешного проекта.