๐ฌ Golang Ticket Booking System from Scratch โ No Framework (Part 2)
Pada part kedua dari seri "Ticket Booking System with Golang (Tanpa Framework)", kita akan mulai masuk ke bagian yang lebih "serius", yaitu:
- Menyelesaikan fungsi
MarkSeatAsBooked - Menambahkan unit test untuk fungsi tersebut
- Melakukan simulasi *race condition* agar 1 kursi tidak bisa dibooking lebih dari sekali secara bersamaan
- Menambahkan fitur penting lain:
DecrementEventQuotadanInsertBooking - Menambahkan constraint untuk mencegah duplikasi booking
๐ Tujuan
Skenario umum yang sering terjadi dalam sistem booking adalah rebutan seat, misalnya saat beli tiket konser. Kita akan menggunakan transaksi dan locking dari database untuk mencegah race condition dan duplikasi booking.
โ๏ธ Step 1 โ Update Interface dan Implementasi MarkSeatAsBooked
๐ File: repository/booking_repository.go
โ Tambahkan Method pada Interface
type BookingRepository interface {
GetSeatStatus(ctx context.Context, tx *sql.Tx, seatId int) string
MarkSeatAsBooked(ctx context.Context, tx *sql.Tx, seatId int)
DecrementEventQuota(ctx context.Context, tx *sql.Tx, eventId int)
InsertBooking(ctx context.Context, tx *sql.Tx, userId, eventId, seatId int)
}๐ ๏ธ Implementasi Fungsi MarkSeatAsBooked
func (repo *bookingRepository) MarkSeatAsBooked(ctx context.Context, tx *sql.Tx, seatId int) {
SQL := "UPDATE seats SET status = 'BOOKED' WHERE id = ?"
_, err := tx.ExecContext(ctx, SQL, seatId)
helper.PanicIfError(err)
}Penjelasan:
- Kursi akan ditandai sebagai "BOOKED"
- Dibungkus transaksi agar bisa rollback jika terjadi error
๐งช Step 2 โ Unit Test: Fungsi Sukses Booking Kursi
๐ File: repository/booking_repository_test.go
func TestMarkSeatAsBooked(t *testing.T) {
insertEvent(1, 100, "Event 1")
insertSeat(9, 1, "AVAILABLE", "A1")
repo := NewBookingRepository()
tx, err := db.BeginTx(context.Background(), nil)
assert.NoError(t, err)
defer func() {
if r := recover(); r != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
repo.MarkSeatAsBooked(context.Background(), tx, 9)
status := repo.GetSeatStatus(context.Background(), tx, 9)
assert.Equal(t, "BOOKED", status)
}๐ป Step 3 โ Implementasi DecrementEventQuota
๐ ๏ธ Implementasi
func (repo *bookingRepository) DecrementEventQuota(ctx context.Context, tx *sql.Tx, eventId int) {
SQL := "UPDATE events SET quota = quota - 1 WHERE id = ?"
_, err := tx.ExecContext(ctx, SQL, eventId)
helper.PanicIfError(err)
}โ Unit Test
func TestDecrementEventQuota(t *testing.T) {
insertEvent(1, 100, "Event 1")
repo := NewBookingRepository()
tx, err := db.BeginTx(context.Background(), nil)
assert.NoError(t, err)
defer func() {
if r := recover(); r != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
repo.DecrementEventQuota(context.Background(), tx, 1)
var quota int
SQL := "SELECT quota FROM events WHERE id = ?"
err = tx.QueryRowContext(context.Background(), SQL, 1).Scan("a)
assert.NoError(t, err)
assert.Equal(t, 99, quota)
}๐งพ Step 4 โ Insert Booking
๐ ๏ธ Implementasi
func (repo *bookingRepository) InsertBooking(ctx context.Context, tx *sql.Tx, userId, eventId, seatId int) {
SQL := "INSERT INTO bookings (user_id, event_id, seat_id) VALUES (?, ?, ?)"
_, err := tx.ExecContext(ctx, SQL, userId, eventId, seatId)
helper.PanicIfError(err)
}โ Unit Test
func TestInsertBooking(t *testing.T) {
repo := NewBookingRepository()
tx, err := db.BeginTx(context.Background(), nil)
assert.NoError(t, err)
defer func() {
if r := recover(); r != nil {
tx.Rollback()
t.Errorf("Unexpected panic: %v", r)
} else {
tx.Commit()
}
}()
insertUser(1, "Fardan")
repo.InsertBooking(context.Background(), tx, 1, 1, 9)
var userId, eventId, seatId int
SQL := "SELECT user_id, event_id, seat_id FROM bookings WHERE user_id = ? AND event_id = ? AND seat_id = ?"
err = tx.QueryRowContext(context.Background(), SQL, 1, 1, 9).Scan(&userId, &eventId, &seatId)
assert.NoError(t, err)
assert.Equal(t, 1, userId)
assert.Equal(t, 1, eventId)
assert.Equal(t, 9, seatId)
}๐ ๏ธ Step 5 โ Insert Dummy User
func insertUser(id int, name string) {
SQL := "INSERT INTO users (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)"
_, err := db.Exec(SQL, id, name)
helper.PanicIfError(err)
}๐งฉ Kesimpulan
Di part kedua ini kita sudah menyelesaikan:
- Fungsi booking dasar (
MarkSeatAsBooked,DecrementEventQuota,InsertBooking) - Menyiapkan unit test untuk memastikan semua berjalan aman
- Menambahkan proteksi unik di level database
More Articles
You might also like
Membuat Goroutine di Golang
Halo teman-teman! Selamat datang di tutorial pertama tentang Goroutine di Golang. Jika kamu baru belajar pemrograman atau baru mengenal Go, jangan khawatir. Artikel ini akan menjelaskan konsep Goroutine dengan bahasa yang sederhana dan mudah dipahami...
Menunggu Goroutine Selesai dengan WaitGroup
Halo teman-teman! Kita ketemu lagi di seri Golang Goroutine.Setelah sebelumnya kita belajar tentang Race Condition, kali ini kita akan membahas cara menunggu semua Goroutine selesai dengan benar menggunakan sync.WaitGroup. Ini topik penting supaya pr...
Struct dan Struct Method di Golang
Halo teman-teman, kali ini kita bakal bahas salah satu fitur penting di Golang yang sering banget dipakai di dunia backend, yaitu struct dan struct method. Kalau kamu udah pernah pakai OOP di bahasa lain kayak Java atau Python, kamu bisa nganggep str...