Pengantar

Dalam proyek backend Go dengan Fiber dan GORM, model dengan relasi kompleks sering hadir untuk mencerminkan struktur data relasional. Artikel ini menjelaskan bagaimana memetakan struct Go ke tabel GORM secara tepat, melengkapi tag json dan gorm, serta membangun relasi HasOne, HasMany, BelongsTo, dan Many-To-Many. Kita juga akan membahas handler Fiber yang melakukan preloading agar menghindari N+1 query dan menjaga performa.

Mapping Struct Go ke Tabel GORM

Setiap struct yang merepresentasikan tabel harus menggunakan tipe data Go yang sesuai dan tag untuk mengontrol perilaku GORM. Secara umum, gunakan gorm.Model bila Anda membutuhkan kolom bawaan (ID, CreatedAt, dll) tetapi jangan ragu mendefinisikan kolom eksplisit untuk kontrol penuh.

Contoh struktur domain sederhana: Penulis memiliki profil, posting, komentar, dan label. Tag json memudahkan serialisasi, sedangkan gorm mengatur nama kolom, kunci asing, dan constraint tambahan.

type Author struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Email     string         `gorm:"uniqueIndex;size:150;not null" json:"email"`
    Profile   AuthorProfile  `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"profile"`
    Posts     []Post         `gorm:"foreignKey:AuthorID" json:"posts"`
}

type AuthorProfile struct {
    ID        uint   `gorm:"primaryKey" json:"id"`
    AuthorID  uint   `gorm:"uniqueIndex" json:"author_id"`
    Bio       string `gorm:"type:text" json:"bio"`
    Website   string `gorm:"size:255" json:"website"`
}

Penjelasan:

  • Tag gorm:"primaryKey": memastikan kolom ID dikenali sebagai primary key.
  • gorm:"size:...": membatasi panjang string, berguna untuk validasi database.
  • constraint: menentukan aksi ON UPDATE/DELETE agar relasi tetap koheren.
  • json: membuat respons API konsisten dengan nama field yang diharapkan konsumen.

Jika Anda ingin nama tabel berbeda dari nama struct, tambahkan method TableName():

func (AuthorProfile) TableName() string {
    return "author_profiles"
}

GORM secara default menggunakan nama tablenya dalam snake_case plural, tetapi method ini memberi kontrol eksplisit ketika Anda bekerja dengan database legacy.

Desain dan Implementasi Relasi Kompleks

Relasi yang umum ditemui di aplikasi nyata dapat digabung dengan jelas jika Anda paham mekanisme GORM. Berikut ringkasan dan contoh untuk setiap jenis relasi:

HasOne (Satu-ke-Satu)

Relasi HasOne terjadi bila satu entitas memiliki tepat satu entitas lain, seperti Author dan AuthorProfile. Tag gorm:"foreignKey:AuthorID" memastikan GORM menggunakan kolom yang tepat ketika melakukan join.

HasMany (Satu-ke-Banyak)

Contoh: satu author bisa memiliki banyak post. Struktur:

type Post struct {
    ID        uint      `gorm:"primaryKey" json:"id"`
    Title     string    `gorm:"size:200;not null" json:"title"`
    Body      string    `gorm:"type:text" json:"body"`
    AuthorID  uint      `gorm:"index" json:"author_id"`
    Author    Author    `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"author"`
    Comments  []Comment `gorm:"foreignKey:PostID" json:"comments"`
    Tags      []Tag     `gorm:"many2many:post_tags" json:"tags"`
}

Perhatikan bahwa AuthorID memuat kunci asing dan GORM secara otomatis menghubungkannya ke Author.

BelongsTo (Banyak-ke-Satu)

Pada relasi BelongsTo, kolom kunci asing berada di model itu sendiri. GORM men-trigger JOIN berdasarkan foreignKey dan references.

Contoh pada Comment:

type Comment struct {
    ID       uint   `gorm:"primaryKey" json:"id"`
    PostID   uint   `gorm:"index" json:"post_id"`
    Post     Post   `gorm:"foreignKey:PostID;references:ID" json:"post"`
    Message  string `gorm:"type:text" json:"message"`
}

Pastikan field Post hanya dipakai bila Anda ingin otomatis memuat data induk saat membutuhkannya.

Many-To-Many (Banyak-ke-Banyak)

Untuk relasi label pada post, struktur Tag dan join table post_tags:

type Tag struct {
    ID    uint    `gorm:"primaryKey" json:"id"`
    Name  string  `gorm:"size:50;uniqueIndex" json:"name"`
    Posts []Post  `gorm:"many2many:post_tags" json:"posts"`
}

GORM akan otomatis mengelola join table post_tags dengan kolom post_id dan tag_id. Jika Anda ingin menambah data tambahan pada join table (misalnya assigned_at), definisikan struct tambahan dengan tag gorm:"foreignKey:..." dan References.

Handler Go Fiber dengan Preloading Relasi

Preloading adalah cara GORM memuat relasi dalam satu query yang kompleks, mencegah N+1. Misalnya, API yang meminta detail author sekaligus profil, posting, komentar, dan tag:

func GetAuthorDetail(c *fiber.Ctx) error {
    authorID := c.Params("id")
    var author Author

    if err := db.Preload("Profile").
        Preload("Posts", func(db *gorm.DB) *gorm.DB {
            return db.Select("id", "title", "author_id", "created_at")
        }).
        Preload("Posts.Comments").
        Preload("Posts.Tags").
        First(&author, "id = ?", authorID).
        Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return fiber.ErrNotFound
        }
        return fiber.NewError(fiber.StatusInternalServerError, err.Error())
    }

    return c.JSON(author)
}

Penjelasan:

  • Preload("Profile"): memuat relasi HasOne.
  • Preload("Posts", func…): menambahkan konfigurasi tambahan (hanya kolom tertentu) agar response tidak berlebihan.
  • Preload("Posts.Comments") dan Preload("Posts.Tags"): memungkinkan preload nested secara bertingkat.
  • First: mengembalikan satu record berdasarkan ID.

Jika Anda ingin menangani daftar data (misalnya semua author), struktur preloading serupa, tetapi gunakan Find:

func ListAuthors(c *fiber.Ctx) error {
    var authors []Author
    if err := db.Preload("Profile").
        Preload("Posts.Comments").
        Preload("Posts.Tags").
        Find(&authors).
        Error; err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, err.Error())
    }
    return c.JSON(authors)
}

Dalam konteks API pagination, tambahkan Limit, Offset, atau Scopes untuk mengontrol jumlah data yang diambil sekaligus.

Menghindari N+1 Query dan Debugging

N+1 query terjadi saat setiap item memicu query tambahan untuk relasinya. Preloading mengatasi ini, tetapi Anda tetap perlu memastikan relasi yang kompleks tidak memuat data berlebihan.

Tips:

  • Gunakan db.Debug() saat local development untuk melihat SQL yang dihasilkan dan memastikan preloading bekerja.
  • Batasi relasi dengan Select atau Omit bila hanya kolom tertentu yang diperlukan.
  • Jangan preload relasi yang tidak digunakan dalam respons karena akan memperlambat query.
  • Perhatikan Limit + Preload: Jika Anda menggunakan Limit, pastikan kondisi limit diterapkan sebelum preloading agar tidak memuat data ekstra.

Kombinasi Fiber + GORM kuat untuk API, tapi pastikan respons Anda mencerminkan kebutuhan konsumen tanpa membebani database.

Kombinasi preloading yang cermat dengan filter/limit membuat API tahan terhadap data besar. Jika query terasa lambat, gunakan EXPLAIN untuk melihat indeks mana yang dipakai, lalu tambahkan indeks tambahan pada kolom foreign key atau kolom yang sering dijadikan filter.

Kesimpulan

Merancang model GORM dengan relasi HasOne, HasMany, BelongsTo, dan Many-To-Many memerlukan pemahaman tag struct serta bagaimana database memetakan foreign key. Penempatan tag json membantu respons API tetap konsisten, sementara tag gorm memastikan perilaku relasi dan constraint. Di sisi handler Fiber, gunakan Preload untuk mengambil data terkait sekaligus, hindari N+1, dan selalu tes query dengan db.Debug() agar Anda bisa melihat SQL aktual. Dengan pola ini, API Anda tetap efisien dan mudah di-maintain.