π¬ 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: DecrementEventQuota dan InsertBooking
* 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
Panduan Lengkap If-Else dan Switch dalam Golang
1. Pengenalan If-Else Dalam bahasa pemrograman Golang, if dan else digunakan untuk pengambilan keputusan dalam suatu program. Dengan if, kita bisa mengevaluasi suatu kondisi, dan berdasarkan hasilnya (benar atau salah), program akan mengeksekusi blok...
Package, Import, dan Access Modifier di Golang
Halo teman-teman developer πHari ini kita akan bahas salah satu fondasi penting dalam bahasa Go: package, import, dan access modifier. Ini adalah konsep dasar yang wajib banget kamu kuasai sebelum masuk ke hal-hal yang lebih kompleks seperti concurr...
π¬ Golang Ticket Booking System from Scratch β No Framework (Part 1)
Pada part pertama ini, kita akan memulai dari nol: Membuat struktur folder project Setup database MySQL Install dependency driver SQL Membuat repository awal Menulis unit test untuk function GetSeatStatus, termasuk test error case π§± Step 1: ...