5
Entity dan Schema Database
Aplikasi ini menyimpan tiga tabel: users, contacts, dan addresses. Bentuk tabel ditentukan oleh berkas migration SQL, bukan oleh AutoMigrate, sehingga schema database dan struct Go harus dijaga tetap sinkron secara manual. Ketiga tabel dibuat berurutan mengikuti nama berkas migration: db/migrations/20231030144428_create_table_users.up.sql:1-101, db/migrations/20231030144435_create_table_contacts.up.sql:1-132, db/migrations/20231030144441_create_table_addresses.up.sql:1-143.
Di sisi Go, ketiga tabel diwakili struct GORM di internal/entity: User pada internal/entity/user_entity.go:4-124, Contact pada internal/entity/contact_entity.go:3-145, dan Address pada internal/entity/address_entity.go:3-14. Setiap struct menutup berkasnya dengan method TableName() yang mengembalikan nama tabel secara eksplisit: "users", "contacts", dan "addresses" (internal/entity/user_entity.go:14-16, internal/entity/contact_entity.go:16-18, internal/entity/address_entity.go:16-18).
Relasi antar tabel
Relasi membentuk rantai satu-ke-banyak: satu users memiliki banyak contacts, dan satu contacts memiliki banyak addresses. Keduanya ditegakkan lewat foreign key di level database, fk_contacts_user_id pada contacts dan fk_addresses_contact_id pada addresses (db/migrations/20231030144435_create_table_contacts.up.sql:1210, db/migrations/20231030144441_create_table_addresses.up.sql:1311).
erDiagram
users {
varchar100 id PK
varchar100 password
varchar100 name
varchar100 token
bigint created_at
bigint updated_at
}
contacts {
varchar100 id PK
varchar100 first_name
varchar100 last_name
varchar100 email
varchar100 phone
varchar100 user_id FK
bigint created_at
bigint updated_at
}
addresses {
varchar100 id PK
varchar100 contact_id FK
varchar255 street
varchar255 city
varchar255 province
varchar10 postal_code
varchar100 country
bigint created_at
bigint updated_at
}
users ||--o{ contacts : "fk_contacts_user_id"
contacts ||--o{ addresses : "fk_addresses_contact_id"
%% fig: Relasi ketiga tabel beserta kolom dan foreign key
%% desc: Satu users memiliki banyak contacts dan satu contacts memiliki banyak addresses, dengan primary key varchar(100) dan foreign key yang dideklarasikan di berkas migration.Di sisi GORM, relasi yang sama dinyatakan lewat tag pada field struct, bukan lewat query manual. User.Contacts memakai foreignKey:user_id;references:id, Contact.User memakai tag yang sama untuk arah sebaliknya, Contact.Addresses memakai foreignKey:contact_id;references:id, dan Address.Contact menunjuk balik ke contacts dengan tag yang sama (internal/entity/contact_entity.go:12-1312, internal/entity/address_entity.go:1313).
Kolom tabel users
| Field | Kolom | Tipe SQL | Null | Tag GORM |
|---|---|---|---|---|
ID | id | varchar(100) | not null | column:id;primaryKey |
Password | password | varchar(100) | not null | column:password |
Name | name | varchar(100) | not null | column:name |
Token | token | varchar(100) | null | column:token |
CreatedAt | created_at | bigint | not null | column:created_at;autoCreateTime:milli |
UpdatedAt | updated_at | bigint | not null | column:updated_at;autoCreateTime:milli;autoUpdateTime:milli |
Contacts | — | — | — | foreignKey:user_id;references:id |
Nama kolom di tag GORM sama persis dengan urutan kolom pada create table users (internal/entity/user_entity.go:5-1114, db/migrations/20231030144428_create_table_users.up.sql:2-915).
Kolom tabel contacts
| Field | Kolom | Tipe SQL | Null | Tag GORM |
|---|---|---|---|---|
ID | id | varchar(100) | not null | column:id;primaryKey |
FirstName | first_name | varchar(100) | not null | column:first_name |
LastName | last_name | varchar(100) | null | column:last_name |
Email | email | varchar(100) | null | column:email |
Phone | phone | varchar(100) | null | column:phone |
UserId | user_id | varchar(100) | not null | column:user_id |
CreatedAt | created_at | bigint | not null | column:created_at;autoCreateTime:milli |
UpdatedAt | updated_at | bigint | not null | column:updated_at;autoCreateTime:milli;autoUpdateTime:milli |
User | — | — | — | foreignKey:user_id;references:id |
Addresses | — | — | — | foreignKey:contact_id;references:id |
Perhatikan perbedaan penamaan: field Go memakai bentuk UserId, sedangkan kolomnya user_id (internal/entity/contact_entity.go:916, db/migrations/20231030144435_create_table_contacts.up.sql:817).
Kolom tabel addresses
| Field | Kolom | Tipe SQL | Null | Tag GORM |
|---|---|---|---|---|
ID | id | varchar(100) | not null | column:id;primaryKey |
ContactId | contact_id | varchar(100) | not null | column:contact_id |
Street | street | varchar(255) | null | column:street |
City | city | varchar(255) | null | column:city |
Province | province | varchar(255) | null | column:province |
PostalCode | postal_code | varchar(10) | null | column:postal_code |
Country | country | varchar(100) | null | column:country |
CreatedAt | created_at | bigint | not null | column:created_at;autoCreateTime:milli |
UpdatedAt | updated_at | bigint | not null | column:updated_at;autoCreateTime:milli;autoUpdateTime:milli |
Contact | — | — | — | foreignKey:contact_id;references:id |
postal_code adalah satu-satunya kolom teks pendek di tabel ini, varchar(10) (internal/entity/address_entity.go:4-1318, db/migrations/20231030144441_create_table_addresses.up.sql:3-1219).
Konvensi yang berlaku di ketiga entity
Tiga hal berulang di semua struct: ID selalu bertipe string dengan tag primaryKey, timestamp disimpan sebagai int64 milidetik lewat autoCreateTime:milli dan autoUpdateTime:milli, dan setiap struct menutup berkasnya dengan TableName() (internal/entity/address_entity.go:420, internal/entity/address_entity.go:11-1621).
Karena ID bukan auto increment, nilai ID dibuat di lapisan pemanggil. UserUseCase mengisi ID dari request.ID milik event, sementara helper test membuat uuid.NewString() untuk Contact dan Address (internal/usecase/user_usecase.go:89-9322, test/helper_test.go:41-4823). Panjang UUID 36 karakter masih masuk ke varchar(100).
⚠️ Kolom yang
nulldi SQL tetap bertipestringbiasa di struct, misalnyatoken,last_name,phone(internal/entity/user_entity.go:824, internal/entity/contact_entity.go:6-825). Konsekuensinya aplikasi tidak dapat membedakan NULL dari string kosong saat membaca baris.
Field enum tidak dipakai di sini; seluruh kolom teks, sehingga perubahan tipe harus dilakukan serentak di berkas migration dan tag struct.
Foreign key dipakai di query
Kolom foreign key bukan sekadar deklarasi. ContactRepository.FindByIdAndUserId memfilter id = ? AND user_id = ? (internal/repository/contact_repository.go:22-2426), dan AddressRepository.FindByIdAndContactId memfilter id = ? AND contact_id = ? (internal/repository/address_repository.go:21-2327). Pola ini memastikan data anak hanya dapat diakses lewat induknya. Detail method repository dibahas di Lapisan Repository.
Urutan hapus dan migrasi turun
Foreign key memaksa urutan operasi. Helper test menghapus dari anak ke induk: ClearAll memanggil ClearAddresses, lalu ClearContact, lalu ClearUsers (test/helper_test.go:12-1628). Arah sebaliknya gagal karena baris induk masih dirujuk.
graph TD
Clear["ClearAll"] --> Addr["ClearAddresses: Delete entity.Address"]
Addr --> Cont["ClearContact: Delete entity.Contact"]
Cont --> Usr["ClearUsers: Delete entity.User"]
style Clear fill:#f59e0b,color:#000
style Addr fill:#2d7d46,color:#fff
style Cont fill:#8b5cf6,color:#fff
style Usr fill:#336791,color:#fff
%% fig: Urutan hapus data mengikuti arah foreign key
%% desc: ClearAll menghapus addresses lebih dulu, lalu contacts, lalu users agar tidak melanggar foreign key.Berkas migration turun memakai drop table sederhana per tabel: users (db/migrations/20231030144428_create_table_users.down.sql:129), contacts (db/migrations/20231030144435_create_table_contacts.down.sql:130), dan addresses (db/migrations/20231030144441_create_table_addresses.down.sql:131).
⚠️
drop table users;akan ditolak MySQL bila baris dicontactsmasih merujuk ke tabel itu, karenafk_contacts_user_id. Untuk rollback manual, turunkan tabel dari anak ke induk, bukan dari induk ke anak. Perintah migration dijelaskan di Menjalankan Aplikasi, Migrasi, dan Test.
Koneksi GORM yang membaca schema ini dibuat sekali saat bootstrap lewat NewDatabase; jalur inisialisasi tersebut ada di Konfigurasi dan Bootstrap. Pemetaan entity ke model dan response dibahas di Lapisan Usecase, Model, dan Converter.