Developer's Development

3.4.7 [Django] Model-DB 연동 본문

AI 활용 애플리케이션 개발/Django Framework

3.4.7 [Django] Model-DB 연동

mylee 2025. 10. 19. 22:13
Django 모델의 개념

 

Django 모델은 데이터베이스 구조(테이블)을 정의하는 역할을 한다.

Python 클래스를 사용하여 정의하며, Django의 ORM(Object-Relational Mapping)을 통해 데이터베이스와 상호작용한다.

 

  • Django ORM의 특징

SQL 쿼리를 작성하지 않아도 객체 지향적으로 데이터베이스 작업이 가능하다.

데이터베이스 독립성을 제공하여 여러 DBMS를 지원한다.

모델을 통해 데이터의 유효성 검사 및 제약 조건을 설정할 수 있다.

 

  • ORM으로 데이터 조작

1. 데이터 추가

1) create() 메서드

  - 매개변수: 필드 이름과 값을 키워드 인수로 전달

  - 반환값: 생성된 객체

2) 객체 생성 후 저장

  - 객체를 생성한 후 .save() 메서드 호출로 데이터베이스에 저장한다.

 

2. 데이터 조회

1) all() 메서드로 모든 데이터 조회

  - 반환값: 쿼리셋(QuerySet), 데이터베이스에서 검색된 객체의 집합

2) filter() 메서드를 이용한 필터링

  - 반환값: 조건에 맞는 객체들의 쿼리셋

3) get() 메서드를 이용한 단일 객체 조회

  - 반환값: 조건에 맞는 단일 객체 (조건이 충족되지 않으면 예외 발생)

4) order_by() 메서드로 정렬

  - 반환값: 정렬된 쿼리셋

 

3. 데이터 수정

1) 객체를 가져와 수정

  - 특정 객체를 조회한 후 필드를 변경하고 .save() 메서드를 호출한다.

 

4. 데이터 삭제

1) 객체 삭제

  - 특정 객체를 조회한 후 .delete() 메서드를 호출해 삭제한다.

 

👉🏻 프로젝트 및 앱 생성

django-admin startproject _03_django_orm
cd _03_django_orm
django-admin startapp post

pip install mysqlclient

 

👉🏻 MySQL Workbench

root 계정에서 스키마 생성

create user 'django'@'%' identified by 'django';
create database djangodb character set utf8mb4 collate utf8mb4_unicode_ci;
grant all privileges on djangodb.* to 'django'@'%';

 

👉🏻 settings.py 수정

앱 등록 및 데이터베이스 설정

# 수정
INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'post'	# 방금 만들어준 앱 등록
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'djangodb',
        'USER': 'django',
        'PASSWORD': 'django',
        'HOST': '127.0.0.1',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4'
        }
    }
}

# 추가
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler'
        }
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['console']
        }
    }
}

# 수정
LANGUAGE_CODE = 'ko-kr'

TIME_ZONE = 'Asia/Seoul'

 

👉🏻 모델 정의

post/models.py 수정

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.title

 

👉🏻 데이터베이스 반영

마이그레이션 파일 생성 및 데이터베이스에 적용

python manage.py makemigrations
python manage.py migrate

 

👉🏻 post/migrations/0001_initial.py

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Post',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('title', models.CharField(max_length=100)),
                ('content', models.TextField()),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
        ),
    ]

 

👉🏻 Django Shell을 활용한 데이터베이스 CRUD 확인

아래 코드 맨 위 코드 두 줄이랑 >>> 부분 shell 명령어 입력

Shell 종료: Ctrl+Z → Enter

cd _03_django_orm
python manage.py shell
7 objects imported automatically (use -v 2 for details).

Ctrl click to launch VS Code Native REPL
Python 3.12.11 | packaged by Anaconda, Inc. | (main, Jun  5 2025, 12:58:53) [MSC v.1929 64 bit (AMD64)] on win32    
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)

>>> from post.models import Post
>>> Post
<class 'post.models.Post'>

>>> Post.objects
<django.db.models.manager.Manager object at 0x0000015C6F68A330>

>>> Post.objects.all()
(0.000)
                SELECT VERSION(),
                       @@sql_mode,
                       @@default_storage_engine,
                       @@sql_auto_is_null,
                       @@lower_case_table_names,
                       CONVERT_TZ('2001-01-01 01:00:00', 'UTC', 'UTC') IS NOT NULL
            ; args=None; alias=default
(0.000) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; args=None; alias=default
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` LIMIT 21; args=(); alias=default
<QuerySet []>

>>> post = Post.objects.create(title='Hello World', content='hello everyone 좋은 아침이에요~')
(0.016) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('Hello World', 'hello everyone 좋은 아침이에요~', '2025-10-16 00:21:36.902041', '2025-10-16 00:21:36.902041'); args=['Hello World', 'hello everyone 좋은 아침이에요~', '2025-10-16 00:21:36.902041', '2025-10-16 00:21:36.902041']; alias=default

>>> post = Post.objects.create(title='Hello World', content='hello everyone 좋은 아침이에요~')
(0.016) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('Hello World', 'hello everyone 좋은 아침이에요~', '2025-10-16 00:21:36.902041', '2025-10-16 00:21:36.902041'); args=['Hello World', 'hello everyone 좋은 아침이에요~', '2025-10-16 00:21:36.902041', '2025-10-16 00:21:36.902041']; alias=default

>>> post.title
'Hello World'
>>> post.content
'hello everyone 좋은 아침이에요~'
>>> post.created_at
datetime.datetime(2025, 10, 16, 0, 21, 36, 902041, tzinfo=datetime.timezone.utc)
>>> post.updated_at
datetime.datetime(2025, 10, 16, 0, 21, 36, 902041, tzinfo=datetime.timezone.utc)
>>> post.id
1

>>> Post.objects.create(title='this is 10', content='벌써 10월이네요...')
(0.015) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('this is 10', '벌써 10월이 네요...', '2025-10-16 00:26:19.990654', '2025-10-16 00:26:19.990654'); args=['this is 10', '벌써 10월이네요...', '2025-10-16 00:26:19.990654', '2025-10-16 00:26:19.990654']; alias=default
<Post: this is 10>

>>> Post.objects.create(title='오늘 점심 뭐 먹지?', content='기가 막힌 점심 메뉴 추천 받습니다🤗')
(0.000) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('오늘 점심 뭐 먹지?', '기가 막힌 점심 메뉴 추천 받습니다🤗', '2025-10-16 00:27:13.575977', '2025-10-16 00:27:13.575977'); args=['오늘 점심 뭐  먹지?', '기가 막힌 점심 메뉴 추천 받습니다🤗', '2025-10-16 00:27:13.575977', '2025-10-16 00:27:13.575977']; alias=default
<Post: 오늘 점심 뭐 먹지?>

>>> post2 = Post(title='가을이네요', content='환절기 감기 조심하세요~!!!')
>>> post2.save()
(0.032) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('가을이네요', '환절기 감기 조심하세요~!!!', '2025-10-16 00:31:25.929084', '2025-10-16 00:31:25.929084'); args=['가을이네요', '환절기 감기 조심 하세요~!!!', '2025-10-16 00:31:25.929084', '2025-10-16 00:31:25.929084']; alias=default

>>> queryset = Post.objects.all()
>>> queryset
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` LIMIT 21; args=(); alias=default
<QuerySet [<Post: Hello World>, <Post: this is 10>, <Post: 오늘 점심 뭐 먹지?>, <Post: 가을이네요>]>
>>> queryset.query
<django.db.models.sql.query.Query object at 0x0000015C6BB737D0>
>>> str(queryset.query)
'SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post`'

>>> import sqlparse
>>> print(sqlparse.format(str(queryset.query), reindent=True))
SELECT `post_post`.`id`,
       `post_post`.`title`,
       `post_post`.`content`,
       `post_post`.`created_at`,
       `post_post`.`updated_at`
FROM `post_post`

>>> Post.objects.filter(title='Hello World')
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`title` = 'Hello World' LIMIT 21; args=('Hello World',); alias=default
<QuerySet [<Post: Hello World>]>

>>> Post.objects.filter(created_at__year=2025)
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`created_at` BETWEEN '2024-12-31 15:00:00' AND '2025-12-31 14:59:59.999999' LIMIT 21; args=('2024-12-31 15:00:00', '2025-12-31 14:59:59.999999'); alias=default
<QuerySet [<Post: Hello World>, <Post: this is 10>, <Post: 오늘 점심 뭐 먹지?>, <Post: 가을이네요>]>

>>> Post.objects.all().order_by('title')
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` ORDER BY `post_post`.`title` ASC LIMIT 21; args=(); alias=default
<QuerySet [<Post: Hello World>, <Post: this is 10>, <Post: 가을이네요>, <Post: 오늘 점심 뭐 먹지?>]>

>>> Post.objects.get(id=1)
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 1 LIMIT 21; args=(1,); alias=default
<Post: Hello World>

>>> Post.objects.get(id=100)
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 100 LIMIT 21; args=(100,); alias=default
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "C:\Users\Playdata\AppData\Local\anaconda3\envs\django_env\Lib\site-packages\django\db\models\manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Playdata\AppData\Local\anaconda3\envs\django_env\Lib\site-packages\django\db\models\query.py", line 633, in get
    raise self.model.DoesNotExist(
post.models.Post.DoesNotExist: Post matching query does not exist.

>>> post = Post.objects.get(id=1)
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 1 LIMIT 21; args=(1,); alias=default

>>> Post.objects.get(id=1) == post
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 1 LIMIT 21; args=(1,); alias=default
True

>>> Post.objects.get(id=1) is post
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 1 LIMIT 21; args=(1,); alias=default
False

>>> Post.objects.create(title='Hello World', content='새로운 hello world')
(0.016) INSERT INTO `post_post` (`title`, `content`, `created_at`, `updated_at`) VALUES ('Hello World', '새로운 hello world', '2025-10-16 01:08:43.196605', '2025-10-16 01:08:43.196605'); args=['Hello World', '새로운 hello world', '2025-10-16 01:08:43.196605', '2025-10-16 01:08:43.196605']; alias=default
<Post: Hello World>

>>> Post.objects.get(title='this is 10')
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`title` = 'this is 10' LIMIT 21; args=('this is 10',); alias=default 
<Post: this is 10>

>>> Post.objects.values('title', 'content')
(0.000) SELECT `post_post`.`title` AS `title`, `post_post`.`content` AS `content` FROM `post_post` LIMIT 21; args=(); alias=default
<QuerySet [{'title': 'Hello World', 'content': 'hello everyone 좋은 아침이에요~'}, {'title': 'this is 10', 'content': '벌써 10월이네요...'}, {'title': '오늘 점심 뭐 먹지?', 'content': '기가 막힌 점심 메뉴 추천 받습니다🤗'}, {'title': '가을이네요', 'content': '환절기 감기 조심하세요~!!!'}, {'title': 'Hello World', 'content': '새로운 hello world'}]>

>>> Post.objects.values()
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` LIMIT 21; args=(); alias=default
<QuerySet [{'id': 1, 'title': 'Hello World', 'content': 'hello everyone 좋은 아침이에요~', 'created_at': datetime.datetime(2025, 10, 16, 0, 21, 36, 902041, tzinfo=datetime.timezone.utc), 'updated_at': datetime.datetime(2025, 10, 16, 0, 21, 36, 902041, tzinfo=datetime.timezone.utc)}, {'id': 2, 'title': 'this is 10', 'content': '벌써 10월이네요...', 'created_at': datetime.datetime(2025, 10, 16, 0, 26, 19, 990654, tzinfo=datetime.timezone.utc), 'updated_at': datetime.datetime(2025, 10, 16, 0, 26, 19, 990654, tzinfo=datetime.timezone.utc)}, {'id': 3, 'title': '오늘 점심 뭐 먹지?', 'content': '기가 막힌 점심 메뉴 추천 받습니다🤗', 'created_at': datetime.datetime(2025, 10, 16, 0, 27, 13, 575977, tzinfo=datetime.timezone.utc), 'updated_at': datetime.datetime(2025, 10, 16, 0, 27, 13, 575977, tzinfo=datetime.timezone.utc)}, {'id': 4, 'title': '가을이네요', 'content': '환절기 감기 조심하세요~!!!', 'created_at': datetime.datetime(2025, 10, 16, 0, 31, 25, 929084, tzinfo=datetime.timezone.utc), 'updated_at': datetime.datetime(2025, 10, 16, 0, 31, 25, 929084, tzinfo=datetime.timezone.utc)}, {'id': 5, 'title': 'Hello World', 'content': '새로운 hello world', 'created_at': datetime.datetime(2025, 10, 16, 1, 8, 43, 196605, tzinfo=datetime.timezone.utc), 'updated_at': datetime.datetime(2025, 10, 16, 1, 8, 43, 196605, tzinfo=datetime.timezone.utc)}]>

>>> Post.objects.values('title', 'content').distinct()
(0.000) SELECT DISTINCT `post_post`.`title` AS `title`, `post_post`.`content` AS `content` FROM `post_post` LIMIT 21; args=(); alias=default
<QuerySet [{'title': 'Hello World', 'content': 'hello everyone 좋은 아침이에요~'}, {'title': 'this is 10', 'content': '벌써 10월이네요...'}, {'title': '오늘 점심 뭐 먹지?', 'content': '기가 막힌 점심 메뉴 추천 받습니다🤗'}, {'title': '가을이네요', 'content': '환절기 감기 조심하세요~!!!'}, {'title': 'Hello World', 'content': '새로운 hello world'}]>

>>> from django.db.models.functions import ExtractYear
>>> from django.db.models import Count                
>>> Post.objects.annotate(year=ExtractYear('created_at')).values('year').annotate(count_by_year=Count('year'))
(0.000) SELECT EXTRACT(YEAR FROM CONVERT_TZ(`post_post`.`created_at`, 'UTC', 'Asia/Seoul')) AS `year`, COUNT(EXTRACT(YEAR FROM CONVERT_TZ(`post_post`.`created_at`, 'UTC', 'Asia/Seoul'))) AS `count_by_year` FROM `post_post` GROUP BY 1 ORDER BY NULL LIMIT 21; args=('UTC', 'Asia/Seoul', 'UTC', 'Asia/Seoul'); alias=default
<QuerySet [{'year': None, 'count_by_year': 0}]>

>>> post = Post.objects.get(id=1)
(0.000) SELECT `post_post`.`id`, `post_post`.`title`, `post_post`.`content`, `post_post`.`created_at`, `post_post`.`updated_at` FROM `post_post` WHERE `post_post`.`id` = 1 LIMIT 21; args=(1,); alias=default
>>> post.title
'Hello World'
>>> post.title += ' Morning☀️'
>>> post.title
'Hello World Morning☀️'
>>> post.save()
(0.016) UPDATE `post_post` SET `title` = 'Hello World Morning☀️', `content` = 'hello everyone 좋은 아침이에요~', `crreated_at` = '2025-10-16 00:21:36.902041', `updated_at` = '2025-10-16 01:27:39.052901' WHERE `post_post`.`id` = 1; args=('Hello World Morning☀️', 'hello everyone 좋은 아침이에요~', '2025-10-16 00:21:36.902041', '2025-10-16 01:27:39.0052901', 1); alias=default

>>> post.delete()
(0.000) DELETE FROM `post_post` WHERE `post_post`.`id` IN (1); args=(1,); alias=default
(1, {'post.Post': 1})

 

 

ORM 연관 관계 활용

 

👉🏻 앱 생성

django-admin startapp product

 

👉🏻 settings.py 수정

product app 추가

INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'post',
    'product'
]

 

👉🏻 모델 정의

product/models.py 수정

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    price = models.PositiveIntegerField()
    stock = models.PositiveIntegerField()
    available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name
    
# 1:1
class Discount(models.Model):
    product = models.OneToOneField(
        Product,
        on_delete=models.CASCADE,
        related_name='discount'
    )
    discount_percentage = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        help_text='Discount percentage (e.g., 10.00 for 10%)'
    )
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()

    def __str__(self):
        return f'{self.discount_percentage}% off for {self.product.name}'
    
# 1:N
class Review(models.Model):
    product = models.ForeignKey(
        Product,
        on_delete=models.CASCADE,
        related_name='review'
    )
    user_id = models.PositiveBigIntegerField(
        max_length=50,
        blank=True,
        null=True
    )
    rating = models.PositiveIntegerField(
        default=1,
        help_text='Rating from 1 to 5'
    )
    comment = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f'Review for {self.product.name} by {self.user_id}'

# N:M
class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    products = models.ManyToManyField(
        Product,
        related_name='categories',
        blank=True
    )

    def __str__(self):
        return self.name

 

👉🏻 product/migrations/0001_initial.py

import django.db.models.deletion
from django.db import migrations, models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Product',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100)),
                ('description', models.TextField(blank=True)),
                ('price', models.PositiveIntegerField()),
                ('stock', models.PositiveIntegerField()),
                ('available', models.BooleanField(default=True)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
        ),
        migrations.CreateModel(
            name='Discount',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('discount_percentage', models.DecimalField(decimal_places=2, help_text='Discount percentage (e.g., 10.00 for 10%)', max_digits=5)),
                ('start_date', models.DateTimeField()),
                ('end_date', models.DateTimeField()),
                ('product', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='discount', to='product.product')),
            ],
        ),
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=100, unique=True)),
                ('products', models.ManyToManyField(blank=True, related_name='categories', to='product.product')),
            ],
        ),
        migrations.CreateModel(
            name='Review',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('user_id', models.PositiveBigIntegerField(blank=True, max_length=50, null=True)),
                ('rating', models.PositiveIntegerField(default=1, help_text='Rating from 1 to 5')),
                ('comment', models.TextField(blank=True)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='review', to='product.product')),
            ],
        ),
    ]

 

👉🏻 product/sample.py 추가 (샘플 데이터)

from product.models import Product, Category, Discount, Review


def create_sample_categories():
    # Category 샘플 데이터
    category_data = [
        {"name": "가전"},
        {"name": "스마트폰/태블릿"},
        {"name": "가구/인테리어"},
        {"name": "패션/의류"},
        {"name": "스포츠/레저"},
    ]

    # Category 생성
    for data in category_data:
        category, created = Category.objects.get_or_create(name=data["name"])
        if created:
            print(f"Created category: {category.name}")
        else:
            print(f"Category already exists: {category.name}")


def create_sample_products():
    # 실제 데이터를 기반으로 한 Product 샘플 데이터
    product_data = [
        {
            "name": "iPhone 15 Pro",
            "description": "애플의 최신 스마트폰, 강력한 A17 Pro 칩셋과 뛰어난 카메라 성능.",
            "price": 1490000,
            "stock": 20,
            "available": True,
        },
        {
            "name": "Galaxy Z Flip5",
            "description": "삼성의 폴더블 스마트폰, 컴팩트한 디자인과 혁신적인 힌지.",
            "price": 1350000,
            "stock": 15,
            "available": True,
        },
        {
            "name": "LG 올레드 TV 55인치",
            "description": "최상의 화질을 제공하는 LG 올레드 TV. 영화 감상에 최적화.",
            "price": 1790000,
            "stock": 10,
            "available": True,
        },
        {
            "name": "Dyson V12 무선청소기",
            "description": "다이슨의 최첨단 무선청소기. 강력한 흡입력과 긴 배터리 수명.",
            "price": 899000,
            "stock": 25,
            "available": True,
        },
        {
            "name": "Nintendo Switch OLED 모델",
            "description": "닌텐도의 인기 있는 게임 콘솔, 선명한 OLED 화면.",
            "price": 419000,
            "stock": 30,
            "available": True,
        },
        {
            "name": "MacBook Air 15인치 M2",
            "description": "애플의 초경량 노트북, M2 칩셋 탑재로 향상된 성능.",
            "price": 1890000,
            "stock": 10,
            "available": True,
        },
        {
            "name": "Sony WH-1000XM5",
            "description": "소니의 프리미엄 노이즈 캔슬링 헤드폰, 뛰어난 음질 제공.",
            "price": 499000,
            "stock": 40,
            "available": True,
        },
        {
            "name": "Canon EOS R6 Mark II",
            "description": "캐논의 풀프레임 미러리스 카메라, 프로급 사진 촬영에 적합.",
            "price": 2790000,
            "stock": 5,
            "available": True,
        },
        {
            "name": "Apple Watch Series 9",
            "description": "애플의 스마트워치, 더 밝은 디스플레이와 혁신적인 기능.",
            "price": 599000,
            "stock": 50,
            "available": True,
        },
        {
            "name": "Samsung Bespoke 냉장고",
            "description": "삼성의 맞춤형 디자인 냉장고, 고급스러운 인테리어 연출.",
            "price": 2990000,
            "stock": 8,
            "available": True,
        },
    ]

    # Product 생성
    for data in product_data:
        product, created = Product.objects.get_or_create(
            name=data["name"],
            defaults={
                "description": data["description"],
                "price": data["price"],
                "stock": data["stock"],
                "available": data["available"],
            },
        )
        if created:
            print(f"Created product: {product.name}")
        else:
            print(f"Product already exists: {product.name}")



from django.db import models
from datetime import datetime
import random



def create_sample_reviews():
    # 샘플 Product 데이터 (ID와 매칭)
    products = [
        (1, "iPhone 15 Pro"),
        (2, "Galaxy Z Flip5"),
        (3, "LG 올레드 TV 55인치"),
        (4, "Dyson V12 무선청소기"),
        (5, "Nintendo Switch OLED 모델"),
        (6, "MacBook Air 15인치 M2"),
        (7, "Sony WH-1000XM5"),
        (8, "Canon EOS R6 Mark II"),
        (9, "Apple Watch Series 9"),
        (10, "Samsung Bespoke 냉장고"),
    ]

    # 샘플 리뷰 데이터 (제품 ID별로 0~4개씩 랜덤 생성)
    review_data = {
        1: [
            (1, 5, "살짝 무겁긴 한데, 간지가 장난 아닙니다."),
            (2, 4, "카메라 성능은 최고! 하지만 발열이 좀 있네요."),
            (10, 5, "사진 퀄리티가 대단합니다. 프로급 장비 같아요."),
            (11, 2, "이번 모델은 아쉽네요. 이전 모델이 더 좋았어요ㅠ"),
        ],
        2: [
            (3, 5, "폴더블이라 휴대성 짱! 힌지가 정말 부드럽습니다."),
            (4, 3, "디자인은 예쁜데, 배터리가 빨리 닳아요."),
        ],
        3: [
            (5, 5, "올레드 화질은 정말 최고입니다. 영화 볼 때 몰입감이 대단하네요."),
        ],
        4: [
            (6, 4, "흡입력은 강력하지만 무게가 조금 무겁습니다."),
            (7, 5, "다이슨 청소기 중 가장 만족스러운 모델이에요!"),
        ],
        5: [
            (8, 5, "OLED 화면 너무 선명하고, 게임 플레이하기에 딱이에요!"),
            (9, 4, "휴대 모드가 편리하고 화면도 좋아요."),
            (10, 5, "닌텐도 팬으로서 너무 만족스럽습니다!"),
        ],
        6: [
            (11, 5, "M2 칩셋 정말 빠릅니다. 작업용으로 최고예요."),
        ],
        7: [
            (12, 5, "노이즈 캔슬링이 엄청 강력합니다. 음질도 너무 좋아요."),
            (13, 4, "장시간 사용하면 귀가 좀 아프긴 해요."),
        ],
        9: [
            (15, 5, "손목에 딱 맞고 기능이 정말 많아요. 운동할 때 최고!"),
        ],
        10: [
            (16, 5, "디자인이 집 인테리어랑 너무 잘 어울립니다."),
            (17, 4, "수납공간이 넓고 활용도가 좋아요."),
        ],
    }

    # Review 생성
    for product_id, reviews in review_data.items():
        for review in reviews:
            user_id, rating, comment = review
            Review.objects.create(
                product_id=product_id,
                user_id=user_id,
                rating=rating,
                comment=comment,
                created_at=datetime.now()
            )
            print(f"Review created for Product {product_id} by User {user_id}")


def create_sample_discounts():
    # 샘플 Product 데이터 (ID와 매칭)
    products = [
        (1, "iPhone 15 Pro"),
        (2, "Galaxy Z Flip5"),
        (3, "LG 올레드 TV 55인치"),
        (4, "Dyson V12 무선청소기"),
        (5, "Nintendo Switch OLED 모델"),
    ]

    # 샘플 할인 데이터
    discount_data = [
        {"product_id": 3, "discount_percentage": 10.00, "start_date": "2025-01-05", "end_date": "2025-01-15"},
        {"product_id": 5, "discount_percentage": 15.00, "start_date": "2025-01-10", "end_date": "2025-01-20"},
        {"product_id": 7, "discount_percentage": 20.00, "start_date": "2025-02-01", "end_date": "2025-02-10"},
        {"product_id": 8, "discount_percentage": 5.00, "start_date": "2025-01-12", "end_date": "2025-01-18"},
        {"product_id": 10, "discount_percentage": 25.00, "start_date": "2025-01-15", "end_date": "2025-01-25"},
    ]

    # Discount 생성
    for data in discount_data:
        start_date = datetime.strptime(data["start_date"], "%Y-%m-%d")
        end_date = datetime.strptime(data["end_date"], "%Y-%m-%d")
        Discount.objects.create(
            product_id=data["product_id"],
            discount_percentage=data["discount_percentage"],
            start_date=start_date,
            end_date=end_date,
        )
        print(f"Discount created: {data['discount_percentage']}% off for Product ID {data['product_id']}")

def create_sample_category_products():
    # 샘플 Product 데이터 (ID와 매칭)
    products = [
        (1, "iPhone 15 Pro"),
        (2, "Galaxy Z Flip5"),
        (3, "LG 올레드 TV 55인치"),
        (4, "Dyson V12 무선청소기"),
        (5, "Nintendo Switch OLED 모델"),
        (6, "MacBook Air 15인치 M2"),
        (7, "Sony WH-1000XM5"),
        (8, "Canon EOS R6 Mark II"),
        (9, "Apple Watch Series 9"),
        (10, "Samsung Bespoke 냉장고"),
    ]

    # 샘플 Category 데이터
    categories = [
        {"name": "가전"},
        {"name": "스마트폰/태블릿"},
        {"name": "가구/인테리어"},
        {"name": "패션/액세서리"},
        {"name": "스포츠/레저"},
    ]

    # Category 생성
    for category_data in categories:
        category, _ = Category.objects.get_or_create(name=category_data["name"])

    # Category와 Product 연결 데이터
    category_products = [
        {"category_name": "스마트폰/태블릿", "product_id": 1},
        {"category_name": "스마트폰/태블릿", "product_id": 2},
        {"category_name": "가전", "product_id": 3},
        {"category_name": "가전", "product_id": 4},
        {"category_name": "스포츠/레저", "product_id": 5},
        {"category_name": "가전", "product_id": 6},
        {"category_name": "패션/액세서리", "product_id": 7},
        {"category_name": "스포츠/레저", "product_id": 8},
        {"category_name": "패션/액세서리", "product_id": 9},
        {"category_name": "가구/인테리어", "product_id": 10},
        {"category_name": "가전", "product_id": 7},
        {"category_name": "스마트폰/태블릿", "product_id": 9},
        {"category_name": "스포츠/레저", "product_id": 3},
        {"category_name": "가구/인테리어", "product_id": 8},
        {"category_name": "스마트폰/태블릿", "product_id": 6},
    ]

    # Category와 Product 연결
    for data in category_products:
        category = Category.objects.get(name=data["category_name"])
        category.products.add(data["product_id"])
        print(f"Linked Product ID {data['product_id']} with Category {category.name}")

 

👉🏻 project/urls.py 수정

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('product/', include('product.urls'))
]

 

👉🏻 product/urls.py 추가

from django.urls import path
from product import views

app_name = 'product'

urlpatterns = [
    path('n_1/', views.test_n_1, name='test_n_1'),
    path('1_1/', views.test_1_1, name='test_1_1'),
    path('prefetch/', views.test_prefetch, name='test_prefetch'),
    path('n_m/', views.test_n_m, name='test_prefetch'),
]

 

👉🏻 데이터베이스 반영

마이그레이션 파일 생성 및 데이터베이스에 적용

python manage.py makemigrations
python manage.py migrate

 

👉🏻 Django Shell

python manage.py shell
>>> from product.sample import *
>>> create_sample_categories()
>>> create_sample_products()
>>> create_sample_reviews()
>>> create_sample_discounts()
>>> create_sample_category_products()

 

👉🏻 product/views.py 수정

from django.shortcuts import render
from django.http import HttpResponse
from product.models import Product, Category, Discount, Review
from django.db.models import Avg, Count
from datetime import datetime, timedelta

# 1:N = Product:Review
def test_n_1(request):

    result = ''

    # 1. 특정 제품의 모든 리뷰 select
    # (1) Review 테이블 1번 + Product 테이블 4번 = 총 5번
    # reviews = Review.objects.filter(product_id=1)

    # (2) Product 테이블 1번 + Review 테이블 1번 + Product 테이블 4번 = 총 6번
    # product = Product.objects.get(id=1)
    # reviews = Review.objects.filter(product=product)

    # ★ (3) Product 테이블 1번 + Review 테이블 1번 = 총 2번
    product = Product.objects.get(id=1)
    reviews = product.review.all()

    for review in reviews:
        result += str(review.id) + '/' + review.product.name + '/' + str(review.user_id) + '/' + str(review.rating) + '/' + review.comment + '<br>'

    # 2. 특정 제품의 평균 평점과 리뷰 개수 select
    product = Product.objects.get(id=1)
    avg_rating = product.review.aggregate(avg_rating=Avg('rating'))['avg_rating']
    review_cnt = product.review.count()

    result = f'{product.name}의 리뷰 평균 평점: {avg_rating}({review_cnt}개 리뷰)<br>'

    # 3. 평점이 높은 리뷰(4점 이상)만 select
    product = Product.objects.get(id=1)
    high_rating_reviews = product.review.filter(rating__gte='4')
    for review in high_rating_reviews:
        result += f'[High Rating] {review.user_id}의 {review.comment} ({review.rating}점)<br>'

    # 4. 모든 제품의 평균 평점과 리뷰 개수 select
    products_with_review = Product.objects.annotate(
        avg_rating=Avg('review__rating'),
        review_count = Count('review')
    )
    result = ''
    for product in products_with_review:
        result += f'Product {product.name} | 평균 평점 {product.avg_rating} : 리뷰 개수 {product.review_count}<br>'

    # 5. 특정 기간(한달전~오늘)동안 작성된 리뷰 select
    start_date = datetime.now() - timedelta(weeks=4)
    end_date = datetime.now()
    reviews_by_date = Review.objects.filter(created_at__range=(start_date, end_date))

    for review in reviews_by_date:
        result += str(review.id) + '/' + review.product.name + '/' + str(review.user_id) + '/' + str(review.rating) + '/' + review.comment + '<br>'
    
    return HttpResponse(result)


# 1:1 = Product:Discount
def test_1_1(request):

    result = ''
    now = datetime.now()

    # 1. 특정 제품의 할인 정보 select 
    product_id = 1

    # (1) Discount.objects.get()
    # (2) 할인 정보가 있는 제품이라면 -> Product 제품명 | Discount 할인율
    # (3) 할인 정보가 없는 제품이라면 -> product_id는 할인 안함!
    try:
        discount = Discount.objects.get(product_id=product_id)
        result += f'{discount.product.name} | {discount.discount_percentage}% 할인<br>'
    except Discount.DoesNotExist:
        result += f'product_id {product_id}는 할인 안함!<br>'

    # 2. 할인 중인 모든 제품 select
    # - 현재 시점에 할인 중인 제품 == 조건
    on_sale_discounts = Discount.objects.filter(start_date__lte=now, end_date__gte=now)
    for discount in on_sale_discounts:
        result += f'{discount.product.name} ({discount.discount_percentage}%)<br>'

    # 3. 특정 할인율(20%) 이상인 제품 select
    # - [파격세일!!!] {제품명} ({할인율}%)
    high_discounts = Discount.objects.filter(discount_percentage__gte=20)
    for discount in high_discounts:
        result += f'[파격세일!!!] {discount.product.name} ({discount.discount_percentage}%)<br>'

    # 4. 할인 정보와 함께 모든 제품 정보 select
    # - 할인 정보가 있으면 -> {제품명} ({할인율}% 세일)
    # - 할인 정보가 없으면 -> 할인 안하는 {제품명}
    all_products = Product.objects.all()

    result += '<br>'
    for product in all_products:
        if hasattr(product, 'discount'):
            result += f'{product.name} ({product.discount.discount_percentage}% 세일)<br>'
        else:
            result += f'할인 안하는 {product.name}<br>'

    # 5. 할인 기간이 지난 제품 select
    # - [할인 종료!!!] {제품명} ({할인율}%)
    expired_discounts = Discount.objects.filter(end_date__lt=now)

    result += '<br>'
    for discount in expired_discounts:
        result += f'[할인 종료!!!] {discount.product.name} ({discount.discount_percentage}%)<br>'

    return HttpResponse(result)

# prefetch
# 💧 위 4번의 많은 쿼리 실행 문제롤 해결
# product 테이블 전체 조회 -> product id를 가지고 discount가 있는지 다 조회
def test_prefetch(request):
    result = ''

    products = Product.objects.prefetch_related('discount')
    
    for product in products:
        if hasattr(product, 'discount'):
            result += f'{product.name} ({product.discount.discount_percentage}% 세일)<br>'
        else:
            result += f'할인 안하는 {product.name}<br>'

    return HttpResponse(result)


# N:M = Product:Category
def test_n_m(request):
    result = ''

    # 1. 특정 제품이 속한 모든 카테고리 select
    product_id = 9
    # (1) 출력 예시
    #     Product {제품명}의 category
    #     - {카테고리 1}
    #     - {카테고리 2}
    #     - ...
    product = Product.objects.get(id=product_id)
    result += f'{product.name}의 category<br>'
    for category in product.categories.all():
        result += f'- {category.name}<br>'

    # 2. 특정 카테고리에 속한 모든 제품 정보(이름, 가격, 재고량) select
    category_name = '스마트폰/태블릿'
    # (1) 출력 예시
    #     Category {카테고리명}의 제품
    #     - {제품명} ({가격}원 / 수량: {재고량}개)
    #     - {제품명} ({가격}원 / 수량: {재고량}개)
    #     - ...
    category = Category.objects.get(name=category_name)
    result += '<br>'
    result += f'{category.name}의 제품<br>'
    for product in category.products.all():
        result += f'- {product.name} ({product.price}원 / 수량: {product.stock}개)<br>'

    # 3. 카테고리가 없는 제품 select
    # (1) category가 null인 product 조회
    # (2) 출력 예시
    #     Category 미포함 제품
    #     - {제품명} ({가격}원 / 수량: {재고량}개)
    #     - {제품명} ({가격}원 / 수량: {재고량}개)
    #     - ...
    products_no_category = Product.objects.filter(categories__isnull=True)
    result += '<br>'
    result += 'Category 미포함 제품<br>'
    for product in products_no_category:
        result += f'- {product.name} ({product.price}원 / 수량: {product.stock}개)<br>'

    # 4. 🔥특정 제품에 새 카테고리 추가
    product_id = 9
    new_category_name = 'Seasonal'
    # (1) 힌트: get_or_create()와 add()
    # (2) 출력 예시: {제품명} ({카테고리명1}, {카테고리명2}, ...)
    product = Product.objects.get(id=product_id)
    new_category, created = Category.objects.get_or_create(name=new_category_name)
    product.categories.add(new_category)

    categories = product.categories.all()
    category_names = ', '.join([cat.name for cat in categories])
    result += '<br>'
    result += f'{product.name} ({category_names})<br>'

    # 5. 모든 카테고리와 각 카테고리의 제품 개수 select
    # (1) 출력 예시
    #     - Category {카테고리명}에는 {제품 개수}개의 제품이!
    #     - Category {카테고리명}에는 {제품 개수}개의 제품이!
    #     - Category {카테고리명}에는 {제품 개수}개의 제품이!
    #     - ...
    categories_with_product_count = Category.objects.annotate(product_count=Count('products'))
    result += '<br>'
    for category in categories_with_product_count:
        result += f'- Category {category.name}에는 {category.product_count}개의 제품이!<br>'

    # 6. 여러 카테고리에 속한 제품 select
    # (1) 출력 예시
    #     여러 카테고리에 속한 제품 목록
    #     - {제품명} (Category 개수: {카테고리 개수})
    #     - {제품명} (Category 개수: {카테고리 개수})
    #     - {제품명} (Category 개수: {카테고리 개수})
    #     - ...
    products_in_multiple_categories = Product.objects.annotate(category_count=Count('categories')).filter(category_count__gt=1)
    result += '<br>'
    for product in products_in_multiple_categories:
        result += f'- {product.name} (Category 개수: {product.category_count})<br>'

    return HttpResponse(result)

 

👉🏻 서버 실행 및 결과 확인

ex)

http://127.0.0.1:8000/product/n_1

python manage.py runserver

 

 

관리자 페이지 활용 (Django Admin)

 

  • 관리자 페이지 활성화

1. 관리자 계정 생성

python manage.py createsuperuser
# 사용자이름 입력
# 이메일 주소 입력
# password 입력 (2번)

 

2. 브라우저에서 관리자 페이지 접속

http://127.0.0.1:8000/admin

 

3. DB에서 insert된 data 확인

SELECT * FROM auth_user;

 

  • 관리자 페이지 커스터마이징

admin.py에 모델 등록 및 리스트 필드 표시 설정

from django.contrib import admin
from qna.entity.models import Question, Answer

# 모델 등록
# admin.site.register(Question)
# admin.site.register(Answer)

# 리스트 필드 표시 설정
@admin.register(Question)
class QuestionAdmin(admin.ModelAdmin):
    list_display = ('id', 'subject', 'created_at')
    search_fields = ('subject',)

@admin.register(Answer)
class AnswerAdmin(admin.ModelAdmin):
    pass

 

👉🏻 결과

리스트에 필드 표시, 검색 및 필터링 기능 제공