181. Pengenalan GORM & DB Connection Setup
Memahami konsep Object-Relational Mapping (ORM) dan membuka koneksi database MySQL/PostgreSQL dengan GORM.
Apa itu GORM?
GORM adalah library Object-Relational Mapping (ORM) paling populer untuk Golang. GORM memungkinkan developer berinteraksi dengan database relasional (MySQL, PostgreSQL, SQLite, SQL Server) menggunakan Go Struct tanpa perlu menulis SQL manual secara berulang.
- Auto Migration skema tabel otomatis dari Go Struct.
- Mendukung relasi lengkap (Has One, Has Many, Belongs To, Many To Many).
- Fitur Transactions, Nested Transactions, dan SavePoint.
- Context, Preloading (Eager Loading), dan Callbacks/Hooks.
Instalasi GORM & Driver:
go get gorm.io/gorm go get gorm.io/driver/mysql # Driver MySQL go get gorm.io/driver/postgres # Driver Postgres
Contoh program
package main
import (
"fmt"
)
// Simulasi Koneksi Database GORM
type GormDB struct {
Dialect string
DSN string
}
func OpenGorm(dialect, dsn string) (*GormDB, error) {
fmt.Printf("[GORM] Opening connection to %s DB with DSN: %s\n", dialect, dsn)
return &GormDB{Dialect: dialect, DSN: dsn}, nil
}
func main() {
fmt.Println("=== INITIALIZING GORM DB CONNECTION ===")
dsn := "user:pass@tcp(127.0.0.1:3306)/my_db?charset=utf8mb4&parseTime=True&loc=Local"
db, err := OpenGorm("mysql", dsn)
if err == nil {
fmt.Println("Koneksi Database GORM Berhasil Dibuat!")
_ = db
}
}
182. Defining GORM Models, Struct Tags & AutoMigrate
Mendefinisikan GORM Model, menggunakan gorm.Model base struct, dan menjalankan skema database AutoMigrate.
Mendesain GORM Model & AutoMigrate
Di GORM, tabel database direpresentasikan oleh Struct. GORM menyediakan struct bawaan gorm.Model yang sudah berisi field umum: ID, CreatedAt, UpdatedAt, dan DeletedAt (Soft Delete).
type Product struct {
gorm.Model
Code string `gorm:"type:varchar(50);uniqueIndex;not null"`
Price float64 `gorm:"type:decimal(10,2);default:0"`
}
// Menjalankan Auto Migration (membuat/memperbarui tabel otomatis)
db.AutoMigrate(&Product{})
Contoh program
package main
import (
"fmt"
"time"
)
type GormModel struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type Category struct {
GormModel
Name string `json:"name"`
Slug string `json:"slug"`
}
func main() {
cat := Category{
GormModel: GormModel{ID: 1, CreatedAt: time.Now(), UpdatedAt: time.Now()},
Name: "Electronics",
Slug: "electronics",
}
fmt.Println("=== MODEL CATEGORY GORM DEMO ===")
fmt.Printf("ID: %d | Name: %s | Slug: %s\n", cat.ID, cat.Name, cat.Slug)
fmt.Println("CreatedAt:", cat.CreatedAt.Format("2006-01-02 15:04:05"))
}
183. CRUD Basic Operations: Insert & Single Record Creation
Memasukkan data baru ke database menggunakan db.Create() dan batch insert.
Membuat Record Baru (Insert)
Untuk memasukkan data baru ke database, kita cukup menginstansiasi Struct lalu memanggil db.Create(&object). GORM akan mengisi kembali nilai `ID` auto-increment dan timestamp ke struct tersebut.
users := []User{
{Name: "Budi", Age: 20},
{Name: "Siti", Age: 22},
}
result := db.Create(&users) // Menginsert banyak record sekaligus dalam 1 query SQL!
fmt.Println("Jumlah record terinsert:", result.RowsAffected)
Contoh program
package main
import (
"fmt"
)
type User struct {
ID uint
Name string
Age int
}
func main() {
var usersDB []User
autoIncID := uint(1)
// Simulasi db.Create()
newUser := User{Name: "Eko Kurniawan", Age: 30}
newUser.ID = autoIncID
usersDB = append(usersDB, newUser)
fmt.Println("=== DEMO GORM db.Create() ===")
fmt.Printf("User Terinsert => ID: %d | Name: %s | Age: %d\n", newUser.ID, newUser.Name, newUser.Age)
fmt.Println("Total Rows in DB:", len(usersDB))
}
184. Querying Records (First, Find, Where, Like, Order, Limit)
Mencari dan memfilter data dari database menggunakan metode First, Find, Where, dan pencarian berantai (chaining).
Membaca Data (Querying)
GORM menyediakan API fluent interface untuk query data secara mudah.
| Metode GORM | Penjelasan | Query SQL Setara |
|---|---|---|
db.First(&user) |
Mengambil 1 record pertama berdasarkan urutan Primary Key. | SELECT * FROM users ORDER BY id LIMIT 1 |
db.Find(&users) |
Mengambil seluruh record ke dalam Slice. | SELECT * FROM users |
db.Where("age > ?", 18) |
Menambahkan klausa WHERE dengan prepared statement. | WHERE age > 18 |
db.Order("age desc").Limit(5) |
Pengurutan dan pembatasan jumlah data. | ORDER BY age DESC LIMIT 5 |
Contoh program
package main
import (
"fmt"
)
type Customer struct {
ID int
Name string
City string
}
func main() {
customers := []Customer{
{ID: 1, Name: "Andi", City: "Jakarta"},
{ID: 2, Name: "Budi", City: "Bandung"},
{ID: 3, Name: "Cici", City: "Jakarta"},
}
fmt.Println("=== DEMO GORM WHERE FILTER (City = Jakarta) ===")
for _, c := range customers {
if c.City == "Jakarta" {
fmt.Printf("- Customer ID %d: %s (%s)\n", c.ID, c.Name, c.City)
}
}
}
185. Updates & Deletions (Save, Updates, Delete, Soft Delete)
Mengubah data dengan Save/Updates dan menghapus data dengan Soft Delete (gorm.DeletedAt).
Update & Delete pada GORM
Untuk mengupdate record, kita bisa memilih mengupdate seluruh field (db.Save(&user)) atau hanya field tertentu (db.Model(&user).Updates(User{Name: "New Name"})).
Jika model memiliki field gorm.DeletedAt, pemanggilan db.Delete(&user) TIDAK akan menghapus baris dari tabel MySQL. GORM hanya akan mengisikan kolom `deleted_at` dengan waktu saat ini.
Query pencarian selanjutnya secara otomatis akan mengabaikan record yang sudah di-soft-delete!
db.Unscoped().Where("id = ?", 1).First(&user) // Memaksa membaca data yang ter-soft-delete
Contoh program
package main
import (
"fmt"
"time"
)
type SoftDeleteUser struct {
ID int
Name string
DeletedAt *time.Time
}
func main() {
now := time.Now()
user := SoftDeleteUser{
ID: 10,
Name: "Dewi",
DeletedAt: &now, // Soft deleted
}
fmt.Println("=== DEMO SOFT DELETE GORM ===")
fmt.Printf("User ID %d (%s)\n", user.ID, user.Name)
if user.DeletedAt != nil {
fmt.Println("Status Data: TERHAPUS SECARA SOFT DELETE pada", user.DeletedAt.Format("15:04:05"))
fmt.Println("Query biasa 'db.Find()' akan otomatis menyembunyikan data ini.")
}
}
186. GORM Relationships: Belongs To & Has One
Membuat dan memetakan relasi 1-to-1 antara dua struct (Belongs To & Has One) menggunakan foreign key.
Relasi 1-to-1 di GORM
GORM mendukung pembuatan relasi database antar tabel secara deklaratif.
- Belongs To: Struct child menyimpan `ForeignKey` dari struct parent (misal: User `Belongs To` Company).
- Has One: Struct parent memiliki tepat 1 child (misal: User `Has One` CreditCard).
type Company struct {
ID int
Name string
}
type User struct {
gorm.Model
Name string
CompanyID int // Foreign Key
Company Company // Relasi Belongs To
}
Contoh program
package main
import (
"fmt"
)
type Company struct {
ID int
Name string
}
type Employee struct {
ID int
Name string
CompanyID int
Company Company // Belongs To
}
func main() {
emp := Employee{
ID: 1,
Name: "Rian",
CompanyID: 101,
Company: Company{ID: 101, Name: "PT Tech Indonesia"},
}
fmt.Println("=== RELASI BELONGS TO GORM ===")
fmt.Printf("Karyawan: %s | Perusahaan: %s (FK CompanyID: %d)\n", emp.Name, emp.Company.Name, emp.CompanyID)
}
187. GORM Relationships: Has Many & Many to Many
Mengelola relasi 1-to-N dan N-to-N serta menggunakan db.Preload() untuk eager loading data relasi.
Relasi Has Many & Many to Many
Saat satu record memiliki banyak anak (misal: 1 User punya banyak CreditCard atau 1 User punya banyak Role), kita menggunakan relasi Has Many atau Many to Many.
Secara default, GORM tidak akan mengambil data relasi untuk menghemat memory (Lazy). Untuk mengambil data anak sekaligus saat query, kita WAJIB memanggil db.Preload("NamaFieldRelasi").
var user User
db.Preload("CreditCards").Preload("Roles").First(&user, 1)
// Sekarang user.CreditCards dan user.Roles sudah terisi lengkap!
Contoh program
package main
import (
"fmt"
)
type CreditCard struct {
Number string
}
type UserHasMany struct {
Name string
CreditCards []CreditCard // Has Many
}
func main() {
user := UserHasMany{
Name: "Budi Santoso",
CreditCards: []CreditCard{
{Number: "4567-xxxx-1122"},
{Number: "5412-xxxx-9900"},
},
}
fmt.Println("=== GORM PRELOAD (EAGER LOADING) SIMULATION ===")
fmt.Println("User Name:", user.Name)
fmt.Println("Daftar Kartu Kredit (Preloaded):")
for i, card := range user.CreditCards {
fmt.Printf(" %d. %s\n", i+1, card.Number)
}
}
188. GORM Transactions & Hooks (Callbacks)
Menjaga integritas data transaksi bisnis dengan db.Transaction() dan memanfaatkan GORM Lifecycle Hooks.
Database Transaction & Lifecycle Hooks
Database Transaction memastikan sekumpulan operasi SQL berhasil **semuanya** (Commit) atau gagal **semuanya** (Rollback).
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err // Otomatis Rollback!
}
if err := tx.Create(&account).Error; err != nil {
return err // Otomatis Rollback!
}
return nil // Otomatis Commit!
})
GORM Hooks (Callbacks):
Hooks adalah fungsi yang dipanggil otomatis sebelum/sesudah operasi (misal: BeforeCreate untuk me-hash password).
Contoh program
package main
import (
"fmt"
)
type Account struct {
Balance float64
}
func TransferMoney(from, to *Account, amount float64) error {
if from.Balance < amount {
return fmt.Errorf("ROLLBACK: Saldo pengirim tidak mencukupi")
}
from.Balance -= amount
to.Balance += amount
return nil // COMMIT
}
func main() {
acc1 := &Account{Balance: 500000}
acc2 := &Account{Balance: 100000}
fmt.Println("=== SIMULASI TRANSACTION TRANSFER ===")
err := TransferMoney(acc1, acc2, 200000)
if err == nil {
fmt.Println("TRANSAKSI COMMIT BERHASIL!")
fmt.Printf("Saldo Acc1: %.0f | Saldo Acc2: %.0f\n", acc1.Balance, acc2.Balance)
}
}
189. Advanced GORM: Raw SQL, Exec & Scopes
Mengeksekusi query SQL kustom dengan db.Raw() dan mengisolasi logika query reusable menggunakan Scopes.
Raw SQL & Reusable Scopes
Meskipun menggunakan ORM, terkadang kita butuh mengeksekusi query SQL kompleks secara manual atau menggunakan reusable filter (Scopes).
db.Raw("SELECT id, name FROM users WHERE age > ?", 20).Scan(&results)
db.Exec("UPDATE users SET status = ? WHERE id = ?", "ACTIVE", 10)
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "ACTIVE")
}
// Penggunaan Scopes yang sangat rapi:
db.Scopes(ActiveUsers).Find(&users)
Contoh program
package main
import (
"fmt"
)
type UserScope struct {
Name string
Status string
}
func FilterActive(users []UserScope) []UserScope {
var active []UserScope
for _, u := range users {
if u.Status == "ACTIVE" {
active = append(active, u)
}
}
return active
}
func main() {
list := []UserScope{
{Name: "Budi", Status: "ACTIVE"},
{Name: "Joko", Status: "SUSPENDED"},
{Name: "Siti", Status: "ACTIVE"},
}
activeUsers := FilterActive(list)
fmt.Println("=== DEMO GORM SCOPES (FILTER ACTIVE) ===")
for _, u := range activeUsers {
fmt.Println("Active User:", u.Name)
}
}