Pada artikel ini, kita akan membahas secara lengkap perbedaan antara Query, Exec, dan Prepare pada penggunaan package database/sql di bahasa pemrograman Go. Pengetahuan ini sangat penting untuk menulis kode yang efisien, aman, dan mudah dipelihara ketika berinteraksi dengan database.

Pendahuluan

Go menyediakan package built-in database/sql untuk mengelola koneksi dan operasi database secara umum. Namun, untuk driver spesifik seperti MySQL, kita tetap perlu mengimpor driver pihak ketiga, misalnya github.com/go-sql-driver/mysql.

Query

Fungsi dengan prefix Query digunakan untuk mengeksekusi perintah SQL yang mengembalikan data, seperti SELECT. Ada beberapa varian fungsi ini:

  • Query(query string, args ...any) (*Rows, error)
  • QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
  • QueryRow(query string, args ...any) *Row
  • QueryRowContext(ctx context.Context, query string, args ...any) *Row

Query()

Digunakan untuk mengambil banyak baris data (multiple rows). Biasanya dipakai untuk operasi SELECT yang mengembalikan lebih dari satu record.

rows, err := db.Query("SELECT id, name, balance FROM customers")

Contoh penggunaan:

for rows.Next() {
    var id, name string
    var balance int
    err = rows.Scan(&id, &name, &balance)
    if err != nil {
        panic(err)
    }
    fmt.Println("ID:", id, "Name:", name, "Balance:", balance)
}

QueryContext()

Sama seperti Query(), namun menerima parameter context untuk mengatur timeout, deadline, atau pembatalan operasi.

rows, err := db.QueryContext(context.Background(), "SELECT id, name, balance FROM customers")

QueryRow()

Digunakan untuk mengambil satu baris data saja (single row), misal berdasarkan ID unik.

row := db.QueryRow("SELECT id, name, balance FROM customers WHERE id = ?", 1)
var id, name string
var balance int
err := row.Scan(&id, &name, &balance)
if err != nil {
    panic(err)
}
fmt.Println("ID:", id, "Name:", name, "Balance:", balance)

QueryRowContext()

Sama seperti QueryRow(), namun menerima parameter context.

row := db.QueryRowContext(context.Background(), "SELECT id, name, balance FROM customers WHERE id = ?", 2)
var id, name string
var balance int
err := row.Scan(&id, &name, &balance)

Catatan:
Pada contoh sebelumnya, penggunaan rows, err := db.QueryRow(...) adalah keliru. Fungsi QueryRow mengembalikan objek *Row, bukan *Rows, dan tidak memiliki method Next(). Data langsung di-scan menggunakan Scan().

Exec

Fungsi dengan prefix Exec digunakan untuk mengeksekusi perintah SQL yang tidak mengembalikan baris data, seperti INSERT, UPDATE, atau DELETE.

  • Exec(query string, args ...any) (Result, error)
  • ExecContext(ctx context.Context, query string, args ...any) (Result, error)

Exec()

result, err := db.Exec("UPDATE customers SET balance = ? WHERE id = ?", 5000000, 2)
if err != nil {
    panic(err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
    panic(err)
}
fmt.Println("Rows affected:", rowsAffected)

ExecContext()

Sama seperti Exec(), namun menerima parameter context.

result, err := db.ExecContext(context.Background(), "DELETE FROM customers WHERE id = ?", 3)
rowsAffected, err := result.RowsAffected()

Catatan:
Pada contoh sebelumnya, penggunaan result.RowAffected() adalah keliru. Method yang benar adalah RowsAffected().

Prepare

Fungsi dengan prefix Prepare digunakan untuk membuat prepared statement, yaitu query yang dapat dieksekusi berulang kali dengan parameter berbeda tanpa harus parsing ulang query oleh database. Ini sangat efisien untuk operasi batch.

  • Prepare(query string) (*Stmt, error)
  • PrepareContext(ctx context.Context, query string) (*Stmt, error)

Prepare()

stmt, err := db.Prepare("INSERT INTO customers(name, balance) VALUES(?, ?)")
if err != nil {
    panic(err)
}
defer stmt.Close()

for i := 1; i <= 10; i++ {
    name := fmt.Sprintf("Nama Seseorang ke-%d", i)
    balance := 100000 + i
    _, err := stmt.Exec(name, balance)
    if err != nil {
        panic(err)
    }
}

PrepareContext()

Sama seperti Prepare(), namun menerima parameter context.

stmt, err := db.PrepareContext(context.Background(), "INSERT INTO customers(name, balance) VALUES(?, ?)")
defer stmt.Close()
for i := 1; i <= 10; i++ {
    name := fmt.Sprintf("Nama Seseorang ke-%d", i)
    balance := 100000 + i
    _, err := stmt.ExecContext(context.Background(), name, balance)
    if err != nil {
        panic(err)
    }
}

Catatan:
Pada contoh sebelumnya, parameter id pada INSERT sebaiknya dihilangkan jika sudah auto-increment di database.

Kesimpulan

  • Gunakan Query/QueryContext untuk mengambil banyak data.
  • Gunakan QueryRow/QueryRowContext untuk mengambil satu baris data.
  • Gunakan Exec/ExecContext untuk operasi yang tidak mengembalikan data (INSERT, UPDATE, DELETE).
  • Gunakan Prepare/PrepareContext untuk operasi berulang dengan query yang sama agar lebih efisien.

Referensi