pemrograman

Pengenalan Integrasi Database MySQL pada Golang

Pengenalan Package Database

Pada pemrograman Golang sebenarnya secara default itu memiliki sebuah package bernama database. Package ini memiliki kumpulan standar interface yang menjadikan standar untuk komunikasi ke dalam database sehingga kita bisa langsung membuat program yang akan kita buat itu menggunakan package tersebut untuk mengakses beberapa jenis database apapun dengan kode yang sama. Tetapi yang akan membedakan adalah hanya kode query SQL yang digunakan sesuai dengan database yang digunakan.

diagram integrasi database

Pada kali ini kita akan fokus untuk mengintegrasikan ke dalam database yang kita gunakan yaitu MySQL sebagai Database Management System. Jadi teman-teman usahakan sudah paham atau mempelajari terlebih dahulu dasar-dasar penggunaan MySQL.

Menambahkan Database Driver

Pastikan terlebih dahulu membuat projek folder terlebih dahulu dengan perintah tahapan dibawah ini:

  • Buat folder learn-golang-database-mysql
  • Buka folder tersebut pada Visual Studio Code ataupun sejenisnya
  • Buka terminal lalu ketik go mod init learn-golang-database-mysql untuk menginisialisasi projek golang dengan go mod.

Sebelum kita buat program menggunakan database, kita perlu tambahkan terlebih dahulu database driver-nya. Tanpa database driver, maka package database golang ini tidak akan mengerti apapun karena package driver ini digunakan untuk menerjemahkan interface kontrak yang ada pada golang. Maka kita bisa kunjungi https://golang.org/s/sqldrivers untuk melihat beberapa driver yang sudah ada dan mendukung pada program golang.

Pada driver MySQL terdapat 3 package yang bisa kita gunakan yaitu:

Maka, kita bisa memilih salah satu dari ketiga package driver tersebut. Tentunya sesuai dengan kebutuhan teman-teman mana yang ingin digunakan. Saran kami, biasanya kita melihat dari populernya package tersebut dengan melihat jumlah star dan fork yang ada pada githubnya.

Tahapan selanjutnya yaitu tambahkan module Database MySQL driver ke dalam projek kita dengan mengetik seperti ini

go get -u github.com/go-sql-driver/mysql

maka hasilnya akan seperti dibawah ini

➜  integrasi-golang-database-mysql go get -u github.com/go-sql-driver/mysql        
go: downloading github.com/go-sql-driver/mysql v1.7.1
go: added github.com/go-sql-driver/mysql v1.7.1

Selanjutnya, kita akan mengimport package driver tersebut di dalam program kita yang nantinya akan seperti ini

import(
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

Membuat Koneksi ke Database

Hal pertama yang perlu kita lakukan ketika membuat aplikasi menggunakan database yaitu bagaimana caranya agar bisa melakukan koneksi ke dalam databasenya. Koneksi ke dalam database pada golang itu kita bisa membuat objek sql.DB dengan menggunakan fungsi sql.Open(driver, dataSourceName) yang sudah kita import sebelumnya pada driver mysql. Sedangkan dataSourceName ini biasanya berisi penulisan koneksi ke dalam database tersebut. Berikut dibawah ini penulisannya.

    <username>:<password>@tcp(<host>:<port>)/<database_name>
  • : username yang sudah dibuat untuk dikoneksikan ke dalam program
  • : kata sandi dari koneksi mysql
  • : server/localhost database yang dituju yang akan digunakan
  • : port database yang digunakan
  • <database_name> : nama dari database yang akan kita koneksikan ke dalam program

Jika objek sql.DB sudah tidak digunakan lagi, saran kami untuk segera menutup koneksinya dengan menggunakan fungsi Close().

Kita coba langsung membuat fungsi main dan ketik kode dibawah ini.

package main

import (
	"database/sql"
)

func main() {
	db, err := sql.Open("mysql", "root:belajargolang@tcp(localhost:3306)/belajar-golang")
	if err != nil {
		panic(err)
	}

	defer db.Close()
}

Pastikan teman-teman sudah tersedia MySQL Database pada laptop atau komputernya. Berikut yang perlu disiapkan:

  • Username root dan password belajargolang ini bisa disesuaikan jika sebelumnya sudah ada username dan password bisa dipakai yang sudah ada.
  • Buat database belajar-golang pada MySQL Database yang mana nantinya akan kita pakai pada integrasi golang ini.

Saat kita jalankan program diatas, maka akan terjadi error panic yang mana sql: unknown driver "mysql" ini merupakan error yang muncul ketika kita belum menambahkan package mysql. Maka jangan lupa kita tambahkan package yang tadi sudah dibawah di awal yaitu penambahan package _ "github.com/go-sql-driver/mysql".

Database Pooling

sql.Db pada Golang bukanlah sebuah koneksi ke dalam database melainkan sebuah pool ke database yang dikenal dengan konsep Database Pooling. Dalam hal tersebut, golang akan melakukan management koneksi ke databaes secara otomatis sehinga kita tidak perlu manage koneksi secara manual. Kemampuan database pooling ini kita juga bisa menentukan jumlah minimal dan maksimal koneksi yang dibuat oleh Golang agar kita tidak membanjiri koneksi ke database karena tiap database memiliki maksimal koneksi yang mengakses database tersebut.

Beberapa pengaturan database config pada sql.DB diantaranya sebagai berikut:

MethodKeterangan
SetMaxIdleConns(number)pengeturan berapa jumlah koneksi minimal yang dibuat
SetmaxOpenConns(number)pengaturan berapa jumlah koneksi maksimal yang dibuat
SetConnMaxIdleTime(duration)pengaturan berapa lama koneksi yang sudah tidak digunakan akan dihapus
SetConnMaxLifetime(duration)pengaturan berapa lama koneksi boleh digunakan

Contohnya kita akan update connection diatas awalnya disimpan di main fungsi akan kita pisah menjadi satu fungsi GetConnection() seperti ini

func GetConnection() *sql.DB {
	db, err := sql.Open("mysql", "root:belajargolang@tcp(localhost:3306)/belajar-golang")
	if err != nil {
		panic(err)
	}

	db.SetMaxIdleConns(10)
	db.SetMaxOpenConns(100)
	db.SetConnMaxIdleTime(5 * time.Minute)
	db.SetConnMaxLifetime(60 * time.Minute)
	return db
}

Eksekusi Perintah SQL

Jika kita ingin komunikasi dengan database, maka kita pastikan bisa berkomunikasi dengan database dengan perintah SQL. Pada Golang sudah menyediakan function yang bisa kita gunakan untuk mengirim perintah SQL ke database menggunakan fungsi DB ExecContext(context,sql,params). Ketika kita mengirim perintah SQL, kita juga perlu mengirimkan context yang pernah kita pelajari sebelumnya agar kita bisa mengirim sinyal cancel juga ketika kita ingin membatalkan pengiriman perintah SQL-nya.

Selanjutnya kita coba buat table Customer dibawah ini

CREATE TABLE customer
(
    id VARCHAR(100) NOT NULL,
    name VARCHAR(100) NOT NULL,
    PRIMARY KEY (id)
) ENGINE = InnoDB;

Mengirim Perintah SQL Insert

Setelah kita buat table customer kita akan coba menyimpan data ke dalam database menggunakan perintah SQL Insert, berikut dibawah ini fungsi yang akan kita buat

func InsertIntoDB(ctx context.Context, db *sql.DB) {
	_, err := db.ExecContext(ctx, "INSERT INTO customer(id,name) VALUES('santekno','Santekno');")
	if err != nil {
		panic(err)
	}
	fmt.Println("success insert data to database")
}

Maka untuk memanggil fungsi ini kita perlu update fungsi main menjadi seperti dibawah ini

func main() {
    // initiate context
	ctx := context.Background()

    // initiate get connection to db
	db := GetConnection()
	defer db.Close()

    // get function insert into database
	InsertIntoDB(ctx, db)
}

Mengirim Perintah Query SQL

Operasi SQL tanpa membutuhkan hasil return dari database kita cuku menggunakan perintah Exec atau ExecContext, namun jika kita membutuhkan hasil dari perintah SQL tersebut seperti SELECT maka kita bisa menggunakan fungsi yang berbeda yaitu QueryContext(context,sql,params).

Baiklah, kita langsung buat saja fungsi untuk mengambil data dari database seperti kode dibawah ini.

func GetDataCustomer(ctx context.Context, db *sql.DB) {
	rows, err := db.QueryContext(ctx, "SELECT id, name FROM customer")
	if err != nil {
		panic(err)
	}
	defer rows.Close()

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

Jika kita lihat diatas hasil dari query fungsi tersebut disimpan ke dalam data struct sql.Rows. Rows ini digunakan untuk melakukan interasi terhadap hasil dari query, kita juga bisa melakukan interasi tersebut dengan menggunakan fungsi rows.Next() untuk menampilkan datanya jika return data false artinya sudah tidak ada lagi data yang ada dalam result. Lalu, kalau kita ingin membaca tiap datanya kita bisa gunakan rows.Scan(column...) dan jangan lupa juga rows tersebut perlu kita tutup dengan menggunakan rows.Close().

Untuk memanggil fungsi mengambil data dari database diatas kita perlu memanggil fungsi diatas pada fungsi main.

func main() {
    // initiate context
	ctx := context.Background()

    // initiate get connection to db
	db := GetConnection()
	defer db.Close()

    // get function insert into database
	// InsertIntoDB(ctx, db)

    // get data customer
    GetDataCustomer(ctx, db)
}

Kita komentar dulu pemanggilan fungsi insert data agar kita bisa mencoba untuk mengambil dari database. Maka jika kita sudah menjalankan hasilnya akan seperti dibawah ini

✗ go run main.go
Id :  santekno
Name:  Santekno

Mengenal Tipe Data Column

Kita juga perlu memahami lebih mendalam untuk tabel-tabel dengan tipe data kolom tertentu, saat ini pada projek ini hanya menggunakan tipe data varchar yang mana biasanya kita gunakan untuk tipe data string pada Golang.

Untuk mengenal lebih dalam lagi, kita akan coba menambahkan beberapa kolom yang sudah kita buat pada tabel customer. Berikut ini kita tambahkan atau coba jalankan di SQL terminal untuk mengubah beberapa kolom tambahan.

DELETE FROM customer;
ALTER TABLE customer
ADD COLUMN email VARCHAR(100),
ADD COLUMN balance INTEGER DEFAULT 0,
ADD COLUMN rating DOUBLE DEFAULT 0.0,
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN birth_date DATE,
ADD COLUMN married BOOLEAN DEFAULT false;

Beberapa tipe data yang akan kita pakai sebagai berikut.

Tipe Data DatabaseTipe Data Golang
VARCHAR, CHARstring
INT, BIGINTint32, int64
FLOAT, DOUBLEfloat32, float64
BOOLEANbool
DATE, DATETIME, TIME, TIMESTAMPtime.Time

Selanjutnya kita juga akan memasukkan data customer agar memiliki sampel data yang akan kita pakai nanti.

INSERT INTO customer(id, name, email, balance, rating, birth_date, married)
VALUES('santekno','Santekno','santekno@gmail.com',100000,5.0,'2000-01-01', true);
INSERT INTO customer(id, name, email, balance, rating, birth_date, married)
VALUES('ihsan','Ihsan Arif','ihsanarifr@gmail.com',100000,5.0,'2001-01-01', true);

Kila lihat juga pada fungsi yang telah kita buat pada GetDataCustomer lalu ubah query tersebut menjadi seperti dibawah ini.

func GetDataCustomer(ctx context.Context, db *sql.DB) {
	rows, err := db.QueryContext(ctx, "SELECT id, name, email, balance, rating, birth_date, married, created_at FROM customer")
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	for rows.Next() {
		var id, name, email string
		var balance int32
		var rating float32
		var birthDate, createdAt time.Time
		var married bool
		err := rows.Scan(&id, &name, &email, &balance, &rating, &birthDate, &married, &createdAt)
		if err != nil {
			panic(err)
		}
		fmt.Println("Id : ", id)
		fmt.Println("Name: ", name)
		fmt.Println("Email: ", email)
		fmt.Println("Balance: ", balance)
		fmt.Println("Rating: ", rating)
		fmt.Println("Birth Date: ", birthDate)
		fmt.Println("Married: ", married)
		fmt.Println("Created At: ", createdAt)
	}
}

Saat kita jalankan programnya maka akan terlihat error panic pada scan kolom seperti ini

panic: sql: Scan error on column index 5, name "birth_date": unsupported Scan, storing driver.Value type []uint8 into type *time.Time

goroutine 1 [running]:
main.GetDataCustomer({0x1006302a8?, 0x14000086000?}, 0x60?)
        /Users/ihsanarifr/Documents/ihsan/learn-golang-database-mysql/main.go:59 +0x528
main.main()
        /Users/ihsanarifr/Documents/ihsan/learn-golang-database-mysql/main.go:20 +0x70
exit status 2

Ternyata tipe data birth_date ini tidak didukung oleh Golang saat menerjemahkan tipe data pada database dengan tipe data yang ada di Golang. Lalu kita harus bagaimana? Tenang ini terjadi karena format time pada database-nya tidak dilakukan penerjemahan ke dalam kode. Maka kita perlu ada tambahan saat koneksi ke database-nya seperti ini.

	db, err := sql.Open("mysql", "root:belajargolang@tcp(localhost:3306)/belajar-golang?parseTime=true")

Secara default, Driver MySQL untuk Golang akan melakukan query tipe data DATE, DATETIME, TIMESTAMP menjadi []byte atau []uint8, dimana bisa dikonversikan menjadi string dengan melakukan parsing manual menjadi time.Time. Konversi ini agak menyulitkan ketika kita memiliki kolom yang banyak dan ke depannya tidak scalable, maka ada cara lain yang bisa kita lakukan yaitu dengan menambahkan parameter parseTime=true pada parameter connection.

Jalankan kembali programnya maka akan sukses dan mengeluarkan data seperti dibawah ini.

✗ go run main.go
Id :  ihsan
Name:  Ihsan Arif
Email:  ihsanarifr@gmail.com
Balance:  100000
Rating:  5
Birth Date:  2001-01-01 00:00:00 +0000 UTC
Married:  true
Created At:  2023-07-30 10:40:20 +0000 UTC
Id :  santekno
Name:  Santekno
Email:  santekno@gmail.com
Balance:  100000
Rating:  5
Birth Date:  2000-01-01 00:00:00 +0000 UTC
Married:  true
Created At:  2023-07-30 10:40:11 +0000 UTC

Pengenalan Tipe Data Nullable Type

Pada Golang menggunakan driver SQL Database tidak bisa memahami tipe data NULL di database. Maka khusus kolom yang memiliki NULL pada database akan menjadi masalah jika kita melakukan Scan secara bulat menggunakan tipe data yang direpresentasikan pada Golang. Lalu kita harus bagaimana ya? Tenang ada caranya kok. Sebelum kepada cara, kita akan menambahkan terlebih dahulu data yang memiliki kolom yang NULL agar kita bisa mendapatkan studi kasus yang sesuai dengan insert data null pada database seperti ini.

INSERT INTO customer(id, name, email, balance, rating, birth_date, married)
VALUES('arif','Arif',NULL,100000,5.0,NULL, true);

Ketika kita jalankan program kita, maka akan menghasilkan error scan pada kolom email yang mana data Arif tadi memiliki email yang NULL.

✗ go run main.go
panic: sql: Scan error on column index 2, name "email": converting NULL to string is unsupported

goroutine 1 [running]:
main.GetDataCustomer({0x104d182a8?, 0x14000014090?}, 0x60?)
        /Users/ihsanarifr/Documents/ihsan/learn-golang-database-mysql/main.go:59 +0x528
main.main()
        /Users/ihsanarifr/Documents/ihsan/learn-golang-database-mysql/main.go:20 +0x70
exit status 2

Solusinya kita akan mengkonversikan secara otomatis NULL yang tidak didukung oleh Driver Mysql Golang dengan tipe khusus yang bisa NULL, yaitu ada pada package sql dibawah ini.

Tipe Data GolangTipe Data Nullable
stringdatabase/sql.NullString
booldatabase/sql.NullBool
float64database/sql.NullFloat64
int32database/sql.NullInt32
int64database/sql.NullInt64
time.Timedatabase/sql.NullTable

Maka, kita perlu mengubah sedikit variable-variabel yang memiliki NULL dari fungsi dari GetDataCustomer() dan memberikan pengecekan pada data yang mengembalikan NULL menjadi seperti ini.

func GetDataCustomer(ctx context.Context, db *sql.DB) {
	rows, err := db.QueryContext(ctx, "SELECT id, name, email, balance, rating, birth_date, married, created_at FROM customer")
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	for rows.Next() {
		var id, name string
		var email sql.NullString
		var balance int32
		var rating float32
		var birthDate sql.NullTime
		var createdAt time.Time
		var married bool
		err := rows.Scan(&id, &name, &email, &balance, &rating, &birthDate, &married, &createdAt)
		if err != nil {
			panic(err)
		}
		fmt.Println("Id : ", id)
		fmt.Println("Name: ", name)
		if email.Valid {
			fmt.Println("Email: ", email)
		}
		fmt.Println("Balance: ", balance)
		fmt.Println("Rating: ", rating)
		if birthDate.Valid {
			fmt.Println("Birth Date: ", birthDate)
		}
		fmt.Println("Married: ", married)
		fmt.Println("Created At: ", createdAt)
	}
}

Mengenal SQL Injection

Saat membuat query, kemungkinan kita akan melakukan hardcode perintah SQL di kode Golang kita yang biasanya akan menerima input data dari user dari parameter lalu ditambahkan pada perintah SQL dari input user dan mengirimkan perintah tersebut menggunakan perintah query SQL.

Lalu apa sih SQL Injection itu? SQL Injection adalah sebuath teknik yang menyalahgunakan sebuah celah keamanan yang terjadi dalam lapisan basis data sebuah program. Biasanya dilakukan dengan mengirimkan input (biasanya parameter) dari pengguna dengan perintah yang salah sehingga menyebabkan hasil SQL yang kita buat menjadi tidak valid. SQL Injection ini sangat berbahaya jika kita salah membuat SQL yang bisa jadi tidak aman untuk program atau aplikasi kita dari serangan hacker.

Sebelum melakukan simulasi SQL Injection kita siapkan terlebih dahulu tabel yang nanti akan kita gunakan.

CREATE TABLE user(
    username VARCHAR(100) NOT NULL,
    password VARCHAR(100) NOT NULL,
    PRIMARY KEY (username)
)ENGINE = InnoDB;

# insert data admin that sample data
INSERT INTO user(username,password) VALUES('admin','admin');

Sekarang, kita akan membuat sebuah fungsi yang mana akan melakukan query perinta SQL untuk mengambil data berdasarkan username dan password.

func Login(ctx context.Context, db *sql.DB, username, password string) bool {
	sqlQuery := "SELECT username FROM user WHERE username='" + username + "' AND password='" + password + "' LIMIT 1"
	rows, err := db.QueryContext(ctx, sqlQuery)
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	if rows.Next() {
		var username string
		rows.Scan(&username)
		fmt.Println("Success Login ", username)
		return true
	} else {
		fmt.Println("Failed Login")
	}
	return false
}

Lalu kita akan jalankan dengan menambahkan perintah ini pada fungsi main seperti berikut.

func main() {
	// initiate context
	ctx := context.Background()

	// initiate get connection to db
	db := GetConnection()
	defer db.Close()

	// get function insert into database
	// InsertIntoDB(ctx, db)

	// get data customer
	// GetDataCustomer(ctx, db)

	username := "admin"
	password := "admin"
	Login(ctx, db, username, password)
}

Maka hasilnya pun akan keluar seperti ini.

✗ go run main.go 
Success Login  admin

Lalu kita coba password-nya kita masukkan salah, maka harusnya akan gagal melakukan login.

✗ go run main.go
Failed Login

Berikutnya kita akan langsung mencoba mengganti username dan passwordnya yang biasanya hacker gunakan untuk melakukan SQL Injection.

username := "admin'; #"
password := "salah"

Maka apa yang akan terjadi?

go run main.go
Success Login  admin

Hasilnya pun user admin bisa login dengan password yang salah, ini adalah contoh dari SQL Injection yang mana sangat berbahaya ketika kita menggunakan query perintah SQL seperti dibawah ini. Lalu bagaimana cara mengatasinya dan solusina seperti apa?

  • Jangan membuat SQL secara manual dengan menggabungkan string secara langsung
  • Jika kita membutuhkan parameter pada perintah SQL maka kita bisa menggunakan fungsi Execute atau Query dengan perintah yang akan kita bahas di chapter selanjutnya.

SQL dengan Parameter untuk menangani SQL Injection

Fungsi Exec can Query memiliki parameter tambahan yang bis akita gunakan untuk mensubstitusi parameter tersebut ke dalam SQL query yang kita buat. Maka SQL membutuhkan parameter untuk mengubahnya yaitu dengan menggunakan karakter ? (tanda tanya).

Misalkan contoh query dibawah ini

  • SELECT username FROM user WHERE username=? AND password=? LIMIT 1
  • INSERT INTO user(username, password) VALUES(?,?)
  • UPDATE USER SET password=? WHERE username=?

Nah kita coba ubah pada fungsi Login query perintah SQL-nya disesuaikan dengan contoh diatas maka akan seperti ini.

	sqlQuery := "SELECT username FROM user WHERE username=? AND password=? LIMIT 1"
	rows, err := db.QueryContext(ctx, sqlQuery, username, password)
    if err != nil {
        panic(err)
    }

Lalu ketika kita jalankan kembali program kita dengan username dan password ini.

username := "admin'; #"
password := "salah"

Maka hasilnya akan menjadi error dan gagal untuk melakukan login.

✗ go run main.go
Failed Login

Nah sekarang kita sudah berhasil menangani masalah terhadap SQL Injection yang akan dilakukan oleh beberapa hacker.

Kita coba juga perintah Exec dengan membuat fungsi baru untuk menambahkan user dengan kode dibawah ini.

func Register(ctx context.Context, db *sql.DB, username, password string) bool {
	_, err := db.ExecContext(ctx, "INSERT INTO user(username, password) VALUE(?,?)", username, password)
	if err != nil {
		panic(err)
	}

	fmt.Println("success insert new user")
	return true
}

Ketika kita sudah menambahkan fungsi tersebut maka kita panggil pada fungsi main seperti dibawah ini.

    username := "santekno'; DROP TABLE user; #"
	password := "santekno"
	// Login(ctx, db, username, password)
	Register(ctx, db, username, password)

Hasil dijalankannya program diatas maka akan terlihat sukses seperti ini.

✗ go run main.go
success insert new user

Dalam username terdapat perintah SQL untuk menghapus table dari user tetapi kita lihat pada database ternyata jika kita menggunakan parameter SQL, masukkan username tersebut akan menjadi string yang disimpan langsung sebagai username yang ada pada kolom username database sehingga ini menjadi aman ketika dijalankan dan perintah menghapus tabelnya pun tidak berlaku.

Mengambil Data ID dari Auto Increment

Kadang kita setelah melakukan penambahan data ke dalam database, kita juga ingin mengambil id auto increment yang dihasilkan, sebenarnya kita bisa menggunakan query ulang ke dalam database dengan menggunakan SELECT LAST_INSERT_ID(), tetapi pada pemrograman bahasa Golang lebih dimudahkan yaitu kita tinggal menggunakan fungsi LastInsertId() untuk mendapatkan ID terakhir yang dibuat secara auto increment. Biasanya kembalian dari fungsi Exec itu memiliki object berupa Result, dari Result kita bisa memanggil fungsi LastInsertId() tersebut untuk mengetahui ID terakhir setelah kita melakukan penambahan data ke dalam tabel.

Untuk melakukan simulasinya, kita perlu menambahkan tabel terlebih dahulu dengan perintah dibawah ini.

CREATE TABLE comments
(
    id INT NOT NULL AUTO_INCREMENT,
    email VARCHAR(100) NOT NULL,
    comment TEXT,
    PRIMARY KEY (id)
) ENGINE InnoDB;

Setelah tabel yang kita buat sudah tersedia pada database, kita lanjutkan dengan membuat fungsi yang mana melakukan penambahan data pada tabel comments dengan mengambil id terakhir yang telah ditambahkan. Berikut fungsi yang akan kita buat dibawah ini.

func InsertComment(ctx context.Context, db *sql.DB, email, comment string) int64 {
	sqlQuery := "INSERT INTO comments(email, comment) VALUES(?, ?)"
	result, err := db.ExecContext(ctx, sqlQuery, email, comment)
	if err != nil {
		panic(err)
	}

	insertID, err := result.LastInsertId()
	if err != nil {
		panic(err)
	}

	return insertID
}

Setelah itu tambahkan pada fungsi main sebelum dijalankan program ini sebagai berikut.

	email := "santekno@gmail.com"
	comment := "hello world"
	lastID := InsertComment(ctx, db, email, comment)
	fmt.Println("Last Insert ID: ", lastID)

Maka hasilnya pun akan menampilkan id terakhir saat melakukan penambahan data secara auto increment.

✗ go run main.go
Last Insert ID:  1

Preparing Statement

Prepare statement adalah fungsi query atau exec yang menggunakan parameter yang mana dibawahnya menggunakan prepare statement. Prepare statement disiapkan terlebih dahulu setiap kita melakukan execute suatu query maka hanya berbeda parameternya saja misalkan saat kita melakukan insert data banyak secara langsung. Prepare statement bisa kita lakukan dengan manual tanpa harus menggunakan query atau exec parameter.

Saat kita membuat prepare statement maka secara otomatis akan mengenali koneksi database yang digunakan sehingga ketika kita mengeksekusi prepare statement berkali-kali maka akan menggunakan koneksi yang sama dan lebih efisien karena pembuatan prepare statementnya hanya sekali diawal saja.

Jika kita menggunakan query dan exec dengan parameter, kita tidak bisa menjalani bahwa koneksi yang digunakan akan sama, oleh karena itu bisa jadi prepare statement akan selalu dibuat berkali-kali walaupun kita menggunakan SQL yang sama. Cara untuk membuat prepare statement kita bisa menggunakan fungsi Prepare(context,sql) denga direpresentasikan dalam struct database/sql.Stmt. Seperti resource sql lain, sq.Stmt ini juga harus di Close() jika sudah tidak digunakan lagi.

Baiklah kita akan coba simulasikan menggunakan prepare statement dibawah ini.

func PrepareStatement(ctx context.Context, db *sql.DB) {
	query := "INSERT INTO comments(email,comment) VALUE(?, ?)"
	statement, err := db.PrepareContext(ctx, query)
	if err != nil {
		panic(err)
	}
	defer statement.Close()

	for i := 0; i < 10; i++ {
		email := "Santekno " + strconv.Itoa(i) + "@gmail.com"
		comment := "Komentar ke " + strconv.Itoa(i)
		result, err := statement.ExecContext(ctx, email, comment)
		if err != nil {
			panic(err)
		}
		id, err := result.LastInsertId()
		if err != nil {
			panic(err)
		}
		fmt.Println("comment id ", id)
	}
}

Lalu selanjutnya untuk mencoba melakukan eksekusi perlu ditambahkan fungsi yang telah kita buat ke dalam fungsi main seperti dibawah ini.

func main(){
    ..
    ..
    PrepareStatement(ctx, db)
}

Prepare statement ini sangat berguna sekali ketika kita memiliki perulangan query ke dalam database yang mana query yang dipakai hanya mengubah parameter saja, maka untuk mengurangi pembuatan koneksi yang berlebihan maka dengan prepare statement koneksi yang dibuah hanya sekali saat prepare statement saja sehingga saat pada saat insert data, koneksi yang gunakan hanya sekali saat prepare statement.

Database Transaction

Fitur dari database yang sangat bermanfaat sekali dalam melakukan transaksi data ke dalam database. Secara default, semua perintah SQL yang kita kirim menggunakan Golang akan otomatis di commit ke dalam database secara otomatis atau auto commit. Namun kita juga bisa menggunakan fitur transaksi ini dengan manual commit ke dalam database. Untuk memulai transaksi, kita bisa menggunakan fungsi Begin() dan akan menghasilkan struct Tx yang merepresentasikan transaksi. Struct Tx akan kita gunakan sebagai pengganti DB untuk melakukan transaksi di setiap koneksi ke db misalkan Exec, Query, dan Prepare. Setelah selesai transaksi kita akan menggunakan fungsi Commit() untuk melakukan commit dan Rollback() ketika kita ingin mengembalikan data tersebut ke semua karena suatu hal misalkan karena error disalah satu transaksinya.

Langung saja kita coba bagaimana mengimplementasikan dibawah ini.

func TransactionDatabase(ctx context.Context, db *sql.DB) {
	tx, err := db.Begin()
	if err != nil {
		panic(err)
	}

	query := "INSERT INTO comments(email,comment) VALUE(?, ?)"

	// do transaction
	for i := 0; i < 10; i++ {
		email := "Santekno " + strconv.Itoa(i) + "@gmail.com"
		comment := "Komentar ke " + strconv.Itoa(i)
		result, err := tx.ExecContext(ctx, query, email, comment)
		if err != nil {
			panic(err)
		}
		id, err := result.LastInsertId()
		if err != nil {
			panic(err)
		}
		fmt.Println("comment id ", id)
	}

	err = tx.Commit()
	if err != nil {
		panic(err)
	}
}

Secara proses ini mirip saja ketika kita pakai ExecContext biasa tetapi yang membedakan yaitu kita menggunakan transaksi manual yang mana kita bisa mengendalikan transaksi tersebut mau dijadikan apa saja. Misalkan ketika ada error disuatu transaksi maka tidak akan disimpan di database. Jika kita ingin mencoba eksekusi insert tetapi kita kembalikan ke semula bisa kita pakai tx.Rollback() sehingga data yang sudah diinsert itu tidak akan disimpan ke dalam database.

comments powered by Disqus