π¬ 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...
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...
Interface dan Interface Kosong di Golang
Halo teman-teman dev πKita lanjut lagi ke seri Golang Basic, dan kali ini kita akan bahas salah satu konsep penting di Go: interface, termasuk interface kosong (interface{}). Interface adalah pondasi dari polymorphism di Go. Kalau kamu ngerti konsep...