← Назад к вопросам

Как организовать построение общей модели данных для проекта?

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
  • Тестируйте отношения — убедитесь что каскадное удаление работает
  • Планируйте масштабирование — учтите будущий рост данных
  • Версионируйте схему — миграции — источник истины

Правильная модель данных — основа успешного проекта.

Как организовать построение общей модели данных для проекта? | PrepBro