백엔드 Back-end/장고 Django

Q. 장고 ORM에서 OneToOneField와 ForeignKey에 unique=True를 한 경우 서로 어떤 차이가 있을까?

Tap to restart 2022. 7. 5. 23:00

 

A. 데이터베이스 상으로는 차이가 없다.

 

예제 코드는 DRF CRUD 예제 프로젝트를 활용했다. 

 

데이터베이스 설정만 MySQL로 변경했다. 변경한 예는 아래와 같다.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'test',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}

 

환경은

장고는 3.2.12

파이썬은 3.8 버전이다.

 

ForeignKey

그냥 ForeignKey 모델이다.

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "category"


class Beverage(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    price = models.IntegerField()
    is_available = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "beverage"

 

아래 명령어로 확인해보자.

$ python manage.py makemigrtaions
$ python manage.py migrate

각 테스트 때마다 기존 마이그레이션 파일을 지우고 다시 makemigrations로 생성했고,

기존 데이터베이스를 지우고 새로 migrate했다. 

 

생성되는 마이그레이션 파일은 다음과 같다.

# Generated by Django 3.2.12 on 2022-07-05 13:05

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


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
            options={
                'db_table': 'category',
            },
        ),
        migrations.CreateModel(
            name='Beverage',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('price', models.IntegerField()),
                ('is_available', models.BooleanField(default=False)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.category')),
            ],
            options={
                'db_table': 'beverage',
            },
        ),
    ]

 

데이터베이스 beverage 테이블을 살펴보자.

장고가 이름을 알아서 생성해서 인덱스를 추가한 것을 볼 수 있다.

 

foreignkey로 했을 때 인덱스 목록

 

ForeignKey에 unique=True 추가

이번에는 원래 모델과 동일하고 unique=True만 추가했다.

...
    category = models.ForeignKey(Category, on_delete=models.CASCADE, unique=True)
...

makemigrations로 만든 마이그레이션 파일은 다음과 같다.

...
                ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.category', unique=True)),
...

데이터베이스 beverage 테이블을 살펴보자.

 

foreignkey와 unique=True 설정한 결과

Non_unique가 0으로 바뀌었고, Key_name이 category_id로 깔끔하게 변경된 것을 확인할 수 있다. 

 

OneToOneField

이번에는 OneToOneField로 해보자.

...
    category = models.OneToOneField(Category, on_delete=models.CASCADE)
...

makemigrations로 만든 마이그레이션 파일은 다음과 같다.

...
                ('category', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='app.category')),
...

마이그레이션 파일에도 OneToOneField라고 적혀 있다. 

데이터베이스 beverage 테이블을 살펴보자. 똑같다. ForeignKey에 unique=True를 한 결과와 같다.

 

OneToOneField로 자동으로 만들어진 category_id 인덱스

 

ForeignKey나 OneToOneField나 따로 인덱스를 추가하는 제약조건을 추가하지 않아도 자동으로 추가되는 것을 확인할 수 있다. 

 

같은 이름으로 제약조건을 추가하면 어떻게 될까?

from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "category"


class Beverage(models.Model):
    name = models.CharField(max_length=200)
    category = models.OneToOneField(Category, on_delete=models.CASCADE)
    price = models.IntegerField()
    is_available = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "beverage"
        constraints = [
            models.UniqueConstraint(fields=['category'], name='category_id'),
        ]

makemigrations로 마이그레이션 파일을 생성하면 다음과 같다. 

# Generated by Django 3.2.12 on 2022-07-05 13:19

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


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
            options={
                'db_table': 'category',
            },
        ),
        migrations.CreateModel(
            name='Beverage',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('price', models.IntegerField()),
                ('is_available', models.BooleanField(default=False)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
                ('category', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='app.category')),
            ],
            options={
                'db_table': 'beverage',
            },
        ),
        migrations.AddConstraint(
            model_name='beverage',
            constraint=models.UniqueConstraint(fields=('category',), name='category_id'),
        ),
    ]

맨 마지막 줄에 Unique제약조건이 생성된 것을 확인할 수 있다. 

migrate을 하면 에러가 발생한다.

에러는 중복 에러! category_id란 키 이름이 이미 있다는 거다.

....
File "/Users/taptorestart/workspace/playground/drf_crud/venv/lib/python3.8/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/Users/taptorestart/workspace/playground/drf_crud/venv/lib/python3.8/site-packages/MySQLdb/connections.py", line 254, in query
    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (1061, "Duplicate key name 'category_id'")

왜냐하면 자동으로 생성되는 키 이름이 바로 category_id니까. 

에러는 났지만 그 전까지는 작업이 되어서 테이블을 살펴보면 다음과 같다.

 

 

이름을 다르게 하면 어떻게 될까?

unique_index_category_id로 이름을 바꿔 보았다.

...
    class Meta:
        db_table = "beverage"
        constraints = [
            models.UniqueConstraint(fields=['category'], name='unique_index_category_id'),
        ]

마이그레이션 파일은 다음과 같다.

# Generated by Django 3.2.12 on 2022-07-05 13:23

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


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Category',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
            ],
            options={
                'db_table': 'category',
            },
        ),
        migrations.CreateModel(
            name='Beverage',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=200)),
                ('price', models.IntegerField()),
                ('is_available', models.BooleanField(default=False)),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
                ('category', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='app.category')),
            ],
            options={
                'db_table': 'beverage',
            },
        ),
        migrations.AddConstraint(
            model_name='beverage',
            constraint=models.UniqueConstraint(fields=('category',), name='unique_index_category_id'),
        ),
    ]

마이그레이트를 실행하면 에러 없이 잘 된다. 

 

이름을 다르게 해서 생성된 인덱스

같은 category_id 칼럼으로 만든 두개의 인덱스가 생겼다. 같은 인덱스 두개는 필요 없으니 사실 굳이 제약조건을 추가할 이유는 없다. 

 

내가 원하는 인덱스unique_index_category_id만 남기고 기본 인덱스category_id는 없앨 수 없나?

unique=False, db_index=False, db_constraint=False

따로 따로 적용해봤을 때 기본 인덱스category_id와 unique_index_category_id 둘 다 나왔다.

3가지 모두 적어도 둘 다 나왔다.

 

기본 인덱스를 없애고 내가 원하는 이름으로 추가할 방법은 못 찾았다. 없는 거 같다.

 

공식 문서를 보면 아래처럼 설명이 나온다.

"A one-to-one relationship. Conceptually, this is similar to a ForeignKey with unique=True, but the “reverse” side of the relation will directly return a single object."

출처: 장고 공식 문서 OneToOneField

 

OneToOneField는 ForeignKey with unique=True 한 것과 비슷하다는 거다. 그래서 OneToOneField에 unique=False를 하고 마이그레이션 파일 살펴보면 아무 변화가 없다.