Featured image of post Relasi OneToOne di Django

Relasi OneToOne di Django

Relasi OneToOne di Django digunakan untuk menghubungkan dua model secara eksklusif satu-satu, seperti antara User dan Profile.

Relasi OneToOne

Sebelum memulai Relasi OneToOne ini, pastikan bahwa kita sudah memahami Relasi ForeignKey terlebih dahulu. Kita tidak perlu mengerti Relasi ManyToMany untuk mempelajari relasi OneToOne ini, tetapi disarankan untuk membaca dan memahaminya terlebih dahulu.

Untuk mengerti relasi OneToOne ini, kita sebaiknya sudah nyaman dengan:

  • Membuat model dengan ForeignKey
  • Konsep related_name
  • Forward dan reverse access

Yang Akan Kita Pelajari

Kali ini kita akan fokus pada satu konsep saja yaitu OneToOneField, yang merepresentasikan relasi one-to-one secara strict. Setelah mempelajari konsep ini, kita akan mengerti kapan menggunakannya, perbedaannya dengan ForeignKey, dan use case paling umum, yaitu melakukan extend built-in model User di Django.

Materi kali ini adalah yang paling singkat dan paling sederhana dari ketiga materi DRF mengenai relasi.


Analogi

Bayangkan seseorang dan SIM-nya. Setiap orang punya paling banyak satu SIM, dan setiap SIM milik hanya satu orang saja.

Atau bayangkan sebuah ponsel dan casing-nya. Satu ponsel punya satu casing. Satu casing muat untuk satu ponsel.

Ini berbeda dengan ForeignKey (one-to-many). Orang tua bisa punya banyak anak (ForeignKey), tapi seseorang punya hanya satu SIM saja (OneToOne).


Kenapa ada OneToOneField

Django sudah memiliki model built-in User yang punya field seperti: username, email, first_name, last_name, password, dan beberapa field lainnya.

Tapi bagaimana jika app kita perlu tambahan informasi bio, avatar_url, atau date_of_birth untuk user? Kita punya dua pilihan:

  • Opsi A: Ganti model User Django sepenuhnya. Ini kompleks, harus dilakukan sebelum migration pertama kita, dan bisa membuat third-party packages yang menggunakan model default User menjadi bermasalah.

  • Opsi B: Buat model Profile terpisah yang dihubungkan ke User dengan OneToOneField. Ini sederhana, aman, dan merupakan pendekatan yang paling umum.

Kita akan menggunakan opsi B.


Buat App-nya

1
2
3
cd my-drf-project
source .venv/bin/activate
python manage.py startapp profiles

Register di config/settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
INSTALLED_APPS = [
    # ... Django built-ins ...

    # Third-party
    'rest_framework',

    # Your apps
    'books',
    'articles',
    'blog',
    'bookstore',
    'profiles',
]

Model Profile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# profiles/models.py

from django.conf import settings
from django.db import models


class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='profile',
    )
    bio = models.TextField(blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    website = models.URLField(blank=True)

    def __str__(self):
        return f"Profile of {self.user.username}"

Mari kita bahas satu-persatu.

Apa itu OneToOneField

Di balik layar, OneToOneField adalah ForeignKey dengan constraint UNIQUE. Dia membuat kolom user_id pada tabel profiles_profile, dan constraint UNIQUE memastikan tidak ada dua profile yang menunjuk ke user yang sama.

1
2
3
4
5
6
7
profiles_profile table:
+----+---------+---------------------+----------------+--------------------+
| id | user_id | bio                 | date_of_birth  | website            |
+----+---------+---------------------+----------------+--------------------+
|  1 |    1    | Python developer    | 1990-05-15     | https://alice.dev  |
|  2 |    2    | Learning Django     | NULL           |                    |
+----+---------+---------------------+----------------+--------------------+

Kalau kita mencoba membuat profile kedua untuk user_id 1, database akan menolak dengan pesan IntegrityError.

settings.AUTH_USER_MODEL, sama seperti ForeignKey

Seperti yang sudah disampaikan pada materi Relasi ForeignKey, selalu gunakan settings.AUTH_USER_MODEL daripada import User secara langsung.

on_delete=models.CASCADE, sama seperti ForeignKey

Jika user dihapus, profile-nya juga ikut dihapus. Ini masuk akal karena profile tanpa user itu tidak ada artinya.

Ini perbedaan penting jika dibandingkan dengan penamaan related_name di ForeignKey. Di ForeignKey, seorang user bisa punya banyak posts, jadi kita pakai plural: related_name='posts'. Dengan OneToOneField, seorang user punya hanya satu profile, jadi kita pakai singular: related_name='profile'.

1
2
3
4
5
# ForeignKey: plural, karena bisa ada banyak
user.posts.all()     # return QuerySet (manager)

# OneToOneField: singular, karena cuma ada satu
user.profile         # return Profile object-nya langsung

Pilihan field untuk profile

  • bio = TextField(blank=True) text opsional, gunakan blank=True saja (jangan null=True untuk string field, seperti yang sudah disampaikan di materi Relasi ForeignKey)
  • date_of_birth = DateField(null=True, blank=True) date field opsional, gunakan keduanya, null=True dan blank=True (date field tidak bisa menyimpan empty string, jadi null=True dibutuhkan)
  • website = URLField(blank=True). URLField adalah CharField dengan built-in URL validation. Opsional, jadi blank=True.

Bagaimana Aksesnya Berbeda dari ForeignKey

Ini adalah hal paling penting yang perlu dipahami tentang OneToOneField.

Dengan ForeignKey

Reverse accessor mengembalikan manager, object yang memberikan .all(), .filter(), .count(), dll. Ini karena satu parent bisa punya banyak children.

1
2
3
4
# Return manager (interface QuerySet)
category.posts.all()
category.posts.count()
category.posts.filter(title__contains='Python')

Dengan OneToOneField

Reverse accessor mengembalikan langsung object-nya. Tidak ada .all(), tidak ada QuerySet. Hanya object saja.

1
2
3
4
# Return Profile object-nya langsung
user.profile
user.profile.bio
user.profile.website

Tidak ada parentheses, tidak ada .all(). Seolah-olah profile adalah regular attribute dari user.

Forward access sama aja

Dari sisi Profile, akses user bekerja identik dengan ForeignKey:

1
2
3
4
profile = Profile.objects.get(pk=1)
profile.user                # object User
profile.user.username       # "alice"
profile.user.email          # "alice@example.com"

Ringkasan Perbandingannya

ArahForeignKeyOneToOneField
Forward (child → parent)post.category → single objectprofile.user → single object
Reverse (parent → child)category.posts.all() → QuerySetuser.profile → single object

Handling Profile yang Tidak Ada

Tidak setiap user akan punya profile. Jika user dibuat tapi tidak ada yang membuat Profile untuk mereka, akses user.profile akan raise exception:

1
2
3
4
from django.contrib.auth.models import User

new_user = User.objects.create_user('charlie', password='test123')
new_user.profile  # ❌ raise exception RelatedObjectDoesNotExist (subclass dari Profile.DoesNotExist)

Cara menangani hal ini dengan aman

Opsi 1: Check dengan hasattr

1
2
3
4
if hasattr(user, 'profile'):
    print(user.profile.bio)
else:
    print("No profile yet")

Catatan: hasattr bekerja dengan cara mencoba akses attribute dan catch exception-nya. Agak misleading tapi berfungsi.

Opsi 2: Try/except

1
2
3
4
try:
    bio = user.profile.bio
except Profile.DoesNotExist:
    bio = ""

Opsi 3: Query model Profile-nya langsung

1
2
3
4
5
profile = Profile.objects.filter(user=user).first()
if profile:
    print(profile.bio)
else:
    print("No profile yet")

Opsi 4: get_or_create (buat kalau tidak ada)

1
2
3
profile, created = Profile.objects.get_or_create(user=user)
# profile adalah existing atau newly created Profile
# created adalah True kalau baru dibuat, False kalau sudah ada

Untuk API, umumnya Opsi 4 paling praktis. Kita mau agar setiap user punya profile, jadi buat saja saat diakses pertama kali.


Kapan Menggunakan OneToOneField

Gunakan ketika:

ScenarioKenapa
Extend built-in User Django dengan field tambahanClassic use case, membuat User tetap clean
Memisahkan data yang jarang diakses dari model yang sering di-queryJaga table utama tetap kecil dan cepat
Relasi yang strictly “hanya satu”Orang ↔ Passport, User ↔ Settings

Jangan gunakan ketika:

ScenarioGunakan yang ini
Parent bisa punya ini lebih dari satuForeignKey
Relasi-nya many-to-manyManyToManyField

Cara mudah menentukannya: Tanyakan ke diri kita, “Bisakah parent punya lebih dari satu dari ini?” Kalau jawabnya iya, bahkan hanya secara teoritis, maka gunakan ForeignKey.

1
2
3
4
5
6
7
# ❌ Salah: user bisa punya beberapa alamat pengiriman
class Address(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

# ✅ Benar: gunakan ForeignKey untuk yang "bisa jadi ada beberapa"
class Address(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='addresses')

File Models Lengkap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# profiles/models.py

from django.conf import settings
from django.db import models


class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='profile',
    )
    bio = models.TextField(blank=True)
    date_of_birth = models.DateField(null=True, blank=True)
    website = models.URLField(blank=True)

    def __str__(self):
        return f"Profile of {self.user.username}"

Singkat dan sederhana. Itulah keindahan dari OneToOneField, relasi ini adalah paling focused dari ketiga tipe relasi.


Latihan Praktek

Buat app dan register

1
python manage.py startapp profiles

Tambahkan 'profiles' ke INSTALLED_APPS di config/settings.py.

Tulis model

Copy model dari Bagian 9 ke dalam profiles/models.py.

Buat dan apply migrations

1
2
python manage.py makemigrations profiles
python manage.py migrate

Latihan di Django shell

1
python manage.py shell
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from django.contrib.auth.models import User
from profiles.models import Profile

# --- Buat users ---
alice = User.objects.create_user('alice_profile', password='test123')
bob = User.objects.create_user('bob_profile', password='test123')

# --- Buat profile untuk Alice ---
profile = Profile.objects.create(
    user=alice,
    bio='Python developer and Django enthusiast',
    website='https://alice.dev',
)

# --- Forward access: profile → user ---
print(profile.user)            # <User: alice_profile>
print(profile.user.username)   # "alice_profile"
print(profile.user.email)      # "" (no email set)

# --- Reverse access: user → profile (return object langsung!) ---
print(alice.profile)           # <Profile: Profile of alice_profile>
print(alice.profile.bio)       # "Python developer and Django enthusiast"
print(alice.profile.website)   # "https://alice.dev"

# --- Bandingkan dengan behavior ForeignKey ---
# ForeignKey:  category.posts.all()  → butuh .all(), return QuerySet
# OneToOne:    alice.profile          → tidak perlu .all(), return single object

# --- Bob tidak punya profile, apa yang terjadi? ---
try:
    print(bob.profile)
except Exception as e:
    print(f"Error: {type(e).__name__}: {e}")
    # RelatedObjectDoesNotExist: User has no profile.

# --- Safe check ---
print(Profile.objects.filter(user=bob).exists())  # False

# --- Buat profile dengan get_or_create ---
bobs_profile, created = Profile.objects.get_or_create(
    user=bob,
    defaults={'bio': 'New to Django'},
)
print(created)             # True (baru dibuat)
print(bobs_profile.bio)    # "New to Django"

# Panggil lagi, kali ini sudah ada
bobs_profile, created = Profile.objects.get_or_create(
    user=bob,
    defaults={'bio': 'This will be ignored'},
)
print(created)             # False (sudah ada)
print(bobs_profile.bio)    # "New to Django" (defaults hanya dipakai pas creation)

# --- Coba buat duplicate profile ---
try:
    Profile.objects.create(user=alice, bio='Duplicate!')
except Exception as e:
    print(f"Error: {e}")   # IntegrityError. UNIQUE constraint failed

# --- Update profile Alice ---
alice = User.objects.get(pk=alice.pk) # Clear cached attribute dan pakai DB row
alice.profile.bio = 'Senior Python developer'
alice.profile.save()
print(alice.profile.bio)   # "Senior Python developer"

# --- Delete profile (user tetap survive) ---
alice.profile.delete()
print(Profile.objects.filter(user=alice).exists())  # False
print(User.objects.filter(username='alice_profile').exists())  # True, Alice masih ada

# --- Delete user (profile juga ikut dihapus, via CASCADE) ---
Profile.objects.create(user=alice, bio='Recreated profile')
alice.delete()
print(Profile.objects.filter(user__username='alice_profile').exists())  # False, gone
print(User.objects.filter(username='alice_profile').exists())  # False, juga gone

Simpulan

1
2
3
4
5
6
OneToOneField           →  "exactly one of each", seperti ForeignKey dengan unique=True
Reverse access          →  return object-nya langsung (user.profile), BUKAN QuerySet
related_name            →  gunakan bentuk singular ('profile'), bukan plural
Missing related object  →  raise DoesNotExist. Handle dengan hasattr, try/except, atau get_or_create
Use case paling sering  →  extend model User Django dengan extra field
Test-nya                →  "Bisakah ada lebih dari satu?" → Yes = ForeignKey, No = OneToOneField

Foto cover oleh antonchernyavskiy dari Unsplash

Dibawah Lisensi CC BY-NC-SA 4.0
Dibangun dengan Hugo
Tema Stack dirancang oleh Jimmy