Published on

Golang SQL Database

Authors

Dalam tulisan ini kita akan mempelajari bagaimana menggunakan SQL database di Golang. Mulai dari dasar database/sql, implementasi dalam pembuatan aplikasi, dan sedikit berbagi pengalaman pribadi dalam menggunakan database.

Database yang digunakan dalam tulisan ini menggunakan PostgreSQL, jangan khawatir untuk semua database driver hampir sama sintaksnya jika spesifik driver saya akan jelaskan juga.

Kita akan membuat simple Pokedex application dengan operasi CRUD pada tabel pokemons.

Persiapan

Pertama-tama kita perlu membuat database pokedex dengan script sql sebagai berikut:


_17
-- migration.sql
_17
CREATE TABLE pokemons (
_17
id int NOT NULL,
_17
name varchar(255) NOT NULL,
_17
species varchar(255) NOT NULL,
_17
height decimal(5,2) NOT NULL,
_17
weight decimal(5,2) NOT NULL
_17
);
_17
_17
INSERT INTO pokemons (id, name, species, height, weight) VALUES
_17
(1, 'Bulbasaur', 'Seed Pokémon', 0.7, 6.9),
_17
(2, 'Ivysaur', 'Seed Pokémon', 1, 12),
_17
(3, 'Venusaur', 'Seed Pokémon', 2, 100),
_17
(132, 'Ditto', 'Transform Pokémon', 0.3, 4),
_17
(808, 'Meltan', 'Hex Nut Pokémon', 0.2, 8);
_17
_17
ALTER TABLE pokemons ADD PRIMARY KEY (id);

Kemudian tinggal buat database dan jalankan migration.sql diatas


_23
# buat database pokedex
_23
$ psql -h localhost -Upostgres -W -c "CREATE DATABASE pokedex";
_23
_23
# import migration sql ke database pokedex
_23
$ psql -h localhost -Upostgres -W -d pokedex -a -f migration.sql
_23
-- migration.ssql
_23
CREATE TABLE pokemons (
_23
id int NOT NULL,
_23
name varchar(255) NOT NULL,
_23
species varchar(255) NOT NULL,
_23
height decimal(5,2) NOT NULL,
_23
weight decimal(5,2) NOT NULL
_23
);
_23
CREATE TABLE
_23
INSERT INTO pokemons (id, name, species, height, weight) VALUES
_23
(1, 'Bulbasaur', 'Seed Pokémon', 0.7, 6.9),
_23
(2, 'Ivysaur', 'Seed Pokémon', 1, 12),
_23
(3, 'Venusaur', 'Seed Pokémon', 2, 100),
_23
(132, 'Ditto', 'Transform Pokémon', 0.3, 4),
_23
(808, 'Meltan', 'Hex Nut Pokémon', 0.2, 8);
_23
INSERT 0 5
_23
ALTER TABLE pokemons ADD PRIMARY KEY (id);
_23
ALTER TABLE

Setelah persiapan database selesai sekarang dilanjutkan dengan bootstrap code.


_4
$ mkdir pokedex && cd pokedex
_4
$ touch main.go
_4
$ go mod init example.com/pokedex
_4
go: creating new go.mod: module example.com/pokedex

Memilih database driver

Alhamdulillah tidak sesulit memilih jodoh, untuk PostgreSQL tidak banyak pilihan dan pq merupakan jawaranya untuk PostgreSQL database. Untuk pilihan driver database lain bisa dicari sendiri di daftar driver yang tersedia. Untuk MYSQL driver yang paling kondang yaitu go-sql-driver/mysql

Membuat koneksi database


_25
// main.go
_25
package main
_25
_25
import (
_25
"database/sql"
_25
"log"
_25
_25
// Import the pq driver.
_25
_ "github.com/lib/pq"
_25
)
_25
_25
func main() {
_25
// init koneksi database instance, tetapi masih belum konek ke database server
_25
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
_25
if err != nil {
_25
log.Fatal(err)
_25
}
_25
_25
// validasi konfigurasi dengan ping ke database server
_25
if err = db.Ping(); err != nil {
_25
log.Fatal(err)
_25
}
_25
_25
log.Println("connected successfully!")
_25
}

Penggunaan library pq dalam kode diatas menggunakan blank identifier _ karena kita tidak menggunakan library tersebut secara langsung, tetapi secara tidak langsung via sql package. Pemanggilan dengan cara tersebut hanya menjalankan method init() didalam package pq ini berfungsi untuk mendaftarkan dirinya sendiri ke dalam database/sql. Pola seperti ini merupakan pendekatan baku untuk hampir semua Go sql driver.

Fungsi sql.Open mengembalikan nilai pointer sql.DB, objek value sql.DB ini bukanlah sebuah koneksi database tapi merupakan pool koneksi dengan maksimum koneksi yg bisa diatur menggunakan db.SetMaxOpenConns(integer_value) dan db.SetMaxIdleConns(int_value). Ilustrasi sederhana mengenai koneksi pool ini yaitu ketika kita menggunakan sql.DB maka driver sql akan mengambil 1 koneksi dari pool untuk digunakan dan kondisi total koneksi di pool sejumlah N-1, jika sudah selesai maka koneksi tersebut dikembalikan ke pool untuk digunakan dalam operasi yang lain.

Jika kita menjalankan kode sederhana diatas maka hasilnya seperti dibawah ini:


_5
$ go run main.go
_5
go: finding module for package github.com/lib/pq
_5
go: downloading github.com/lib/pq v1.8.0
_5
go: found github.com/lib/pq in github.com/lib/pq v1.8.0
_5
19:54:15 connected successfully!

Dasar SQL

Kita mulai dengan query receh SELECT * FROM pokemons kemudian kita tampilkan kedalam stdout.


_69
// main.go
_69
package main
_69
_69
import (
_69
"database/sql"
_69
"fmt"
_69
"log"
_69
_69
// Import the pq driver.
_69
_ "github.com/lib/pq"
_69
)
_69
_69
type Pokemon struct {
_69
ID int
_69
Name string
_69
Species string
_69
Height float64
_69
Weight float64
_69
}
_69
_69
func (p Pokemon) String() string {
_69
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg", p.ID, p.Name, p.Species, p.Height, p.Weight)
_69
}
_69
_69
func main() {
_69
// init koneksi database instance, tetapi masih belum konek ke database server
_69
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
_69
if err != nil {
_69
log.Fatal(err)
_69
}
_69
_69
// validasi konfigurasi dengan ping ke database server
_69
if err = db.Ping(); err != nil {
_69
log.Fatal(err)
_69
}
_69
_69
log.Println("connected successfully!")
_69
_69
rows, err := db.Query("SELECT * FROM pokemons")
_69
if err != nil {
_69
log.Fatal(err)
_69
}
_69
_69
defer rows.Close()
_69
_69
pokemons := make([]*Pokemon, 0)
_69
for rows.Next() {
_69
p := new(Pokemon)
_69
err := rows.Scan(
_69
&p.ID,
_69
&p.Name,
_69
&p.Species,
_69
&p.Height,
_69
&p.Weight,
_69
)
_69
if err != nil {
_69
log.Fatal(err)
_69
}
_69
pokemons = append(pokemons, p)
_69
}
_69
_69
if err = rows.Err(); err != nil {
_69
log.Fatal(err)
_69
}
_69
_69
for _, p := range pokemons {
_69
fmt.Println(p.String())
_69
}
_69
}

Apa yang terjadi dengan kode diatas? Kita mendefinisikan tipe Pokemon untuk menampung hasil query database dari tabel pokemons. Tipe data masih menggunakan standar int, string, dan float64 karena didalam database kita paksa nilainya tidak boleh nil dengan NOT NULL ketika mendefinisikan tabel. Jika menggunakan nilai yang nullable maka kita perlu menampungnya kedalam sql.NullString, sql.NullInt32, dan sql.NullFloat64. Untuk menyederhanakan tulisan ini kita hindari dulu penggunaan nullable field.

Kita coba bedah melakukan apa saja kode diatas:

  1. Mengambil resulset dari tabel pokemons menggunakan fungsi DB.Query() dan diletakkan di variabel rows. Setelah itu defer rows.Close() untuk memastikan resulset telah menutup koneksi kedalam database sebelum fungsi parent selesai. Menutup resultset ini sangat penting. Karena jika dibiarkan saja maka koneksi yang digunakan dalam mengambil data diatas tidak dikembalikan ke koneksi pool sehingga mempercepat kehabisan koneksi ke database.
  2. Kemudian menggunakan rows.Next() untuk iterasi semua baris dalam resultset dan dilanjutkan dengan rows.Scan() untuk memindahkan data. Urutan ketika rows.Scan() ini sesuai query, dalam hal ini kita menggunakan SELECT * berarti urutannya sesuai dengan tabel didatabase, jika querynya SELECT c, b, a maka urutanya sesuai dengan deklarasi di SELECT statement yaitu c, b, a.
  3. Ketika rows.Next() loop selesai kita panggil rows.Err(). Ini untuk memastikan jika ada error ketika melakukan iterasi, karena tidak semua iterasi diatas pasti selalu berakhir bahagia tanpa error.
  4. Jika semuanya aman tanpa error maka tinggal loop variabel pokemons dan kita tampilkan informasi kedalam stdout.

Tampilan kode diatas sebagai berikut,


_7
go run main.go
_7
2020/10/09 20:09:18 connected successfully!
_7
1, Bulbasaur, Seed Pokémon, 0.70 m, 6.90 Kg
_7
2, Ivysaur, Seed Pokémon, 1.00 m, 12.00 Kg
_7
3, Venusaur, Seed Pokémon, 2.00 m, 100.00 Kg
_7
132, Ditto, Transform Pokémon, 0.30 m, 4.00 Kg
_7
808, Meltan, Hex Nut Pokémon, 0.20 m, 8.00 Kg

Konversi ke web app

Saatnya aplikasi sederhana diatas henshin ke REST ala-ala dengan 3 routes dan hanya menerima form request:

  • GET /pokemons – daftar semua pokemon di pokedex
  • GET /pokemons/show – menampilkan spesifik pokemon berdasarkan pokemon id
  • POST /pokemons/create – menambahkan pokemon baru ke pokedex

_53
package main
_53
_53
import (
_53
"database/sql"
_53
"fmt"
_53
"log"
_53
"net/http"
_53
"strconv"
_53
_53
// Import the pq driver.
_53
_ "github.com/lib/pq"
_53
)
_53
_53
type Pokemon struct {
_53
ID int
_53
Name string
_53
Species string
_53
Height float64
_53
Weight float64
_53
}
_53
_53
func (p Pokemon) String() string {
_53
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg",
_53
p.ID,
_53
p.Name,
_53
p.Species,
_53
p.Height,
_53
p.Weight)
_53
}
_53
_53
type Env struct {
_53
db *sql.DB
_53
}
_53
_53
func main() {
_53
// init koneksi database instance, tetapi masih belum konek ke database server
_53
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
_53
if err != nil {
_53
log.Fatal(err)
_53
}
_53
_53
// validasi konfigurasi dengan ping ke database server
_53
if err = db.Ping(); err != nil {
_53
log.Fatal(err)
_53
}
_53
log.Println("connected successfully!")
_53
_53
env := &Env{db: db}
_53
http.Handle("/pokemons", pokemonsIndex(env))
_53
http.Handle("/pokemons/show", pokemonsShow(env))
_53
http.Handle("/pokemons/create", pokemonsCreate(env))
_53
http.ListenAndServe(":3000", nil)
_53
}

HTTP handler untuk pembuatan web app kali ini menggunakan pendekatan closure, keutunganya tiap handler lebih bebas dalam menggunakan parameter input. Dalam aplikasi kali ini hanya env yang digunakan sebagai parameter input. Dalam pokemonsShow parsing data hanya menggunakan r.FormValue untuk menyederhanakan pembahasan kali ini, karena lebih fokus ke penggunaan database.


_39
func pokemonsIndex(env *Env) http.Handler {
_39
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_39
_39
if r.Method != "GET" {
_39
http.Error(w, http.StatusText(405), 405)
_39
return
_39
}
_39
rows, err := env.db.Query("SELECT * FROM pokemons")
_39
if err != nil {
_39
log.Fatal(err)
_39
}
_39
_39
defer rows.Close()
_39
_39
pokemons := make([]*Pokemon, 0)
_39
for rows.Next() {
_39
p := new(Pokemon)
_39
err := rows.Scan(
_39
&p.ID,
_39
&p.Name,
_39
&p.Species,
_39
&p.Height,
_39
&p.Weight,
_39
)
_39
if err != nil {
_39
log.Fatal(err)
_39
}
_39
pokemons = append(pokemons, p)
_39
}
_39
_39
if err = rows.Err(); err != nil {
_39
log.Fatal(err)
_39
}
_39
_39
for _, p := range pokemons {
_39
fmt.Fprintln(w, p.String())
_39
}
_39
})
_39
}

Untuk fungsi pokemonsShow memerlukan data dari user yaitu id yang didapat dari r.FormValue("id") dan passing parameter query menggunakan $1 karena menggunakan postgresql, jika menggunakan Mysql maka placeholder $1 perlu diganti dengan ? agar tidak error. Pengambilan datanya hanya sekali tidak seperti di pokemonsIndex.


_27
func pokemonsShow(env *Env) http.Handler {
_27
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_27
if r.Method != "GET" {
_27
http.Error(w, http.StatusText(405), 405)
_27
return
_27
}
_27
_27
id, err := strconv.Atoi(r.FormValue("id"))
_27
if id == 0 || err != nil {
_27
http.Error(w, http.StatusText(400), 400)
_27
return
_27
}
_27
_27
row := env.db.QueryRow("SELECT * FROM pokemons WHERE id = $1", id)
_27
_27
p := new(Pokemon)
_27
err = row.Scan(&p.ID, &p.Name, &p.Species, &p.Height, &p.Weight)
_27
if err == sql.ErrNoRows {
_27
http.NotFound(w, r)
_27
return
_27
} else if err != nil {
_27
http.Error(w, http.StatusText(500), 500)
_27
return
_27
}
_27
fmt.Fprintf(w, p.String())
_27
})
_27
}

Endpoint untuk menambahkan data juga sedikit berbeda dalam pengecekannya karena menggunakan POST request maka selain POST request dianggap error. Kali ini kita menggunakan execute statement karena bukan merupakan query.


_47
func pokemonsCreate(env *Env) http.Handler {
_47
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_47
_47
if r.Method != "POST" {
_47
http.Error(w, http.StatusText(405), 405)
_47
return
_47
}
_47
_47
idStr := r.FormValue("id")
_47
name := r.FormValue("name")
_47
species := r.FormValue("species")
_47
heightStr := r.FormValue("height")
_47
weightStr := r.FormValue("weight")
_47
if idStr == "" || name == "" || species == "" || heightStr == "" || weightStr == "" {
_47
http.Error(w, http.StatusText(400), 400)
_47
return
_47
}
_47
id, err := strconv.Atoi(idStr)
_47
if err != nil {
_47
http.Error(w, http.StatusText(400), 400)
_47
return
_47
}
_47
height, err := strconv.ParseFloat(heightStr, 64)
_47
if err != nil {
_47
http.Error(w, http.StatusText(400), 400)
_47
return
_47
}
_47
weight, err := strconv.ParseFloat(weightStr, 64)
_47
if err != nil {
_47
http.Error(w, http.StatusText(400), 400)
_47
return
_47
}
_47
result, err := env.db.Exec("INSERT INTO pokemons VALUES($1, $2, $3, $4, $5)", id, name, species, height, weight)
_47
if err != nil {
_47
fmt.Fprintf(w, "something wrong", err)
_47
http.Error(w, http.StatusText(500), 500)
_47
return
_47
}
_47
_47
rowsAffected, err := result.RowsAffected()
_47
if err != nil {
_47
http.Error(w, http.StatusText(500), 500)
_47
return
_47
}
_47
fmt.Fprintf(w, "Pokemon %d created successfully (%d row affected)\n", id, rowsAffected)
_47
})
_47
}

Jika semua digabungkan code diatas maka hasilnya seperti ini


_169
package main
_169
_169
import (
_169
"database/sql"
_169
"fmt"
_169
"log"
_169
"net/http"
_169
"strconv"
_169
_169
// Import the pq driver.
_169
_ "github.com/lib/pq"
_169
)
_169
_169
type Pokemon struct {
_169
ID int
_169
Name string
_169
Species string
_169
Height float64
_169
Weight float64
_169
}
_169
_169
func (p Pokemon) String() string {
_169
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg",
_169
p.ID,
_169
p.Name,
_169
p.Species,
_169
p.Height,
_169
p.Weight)
_169
}
_169
_169
type Env struct {
_169
db *sql.DB
_169
}
_169
_169
func main() {
_169
// init koneksi database instance, tetapi masih belum konek ke database server
_169
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
_169
if err != nil {
_169
log.Fatal(err)
_169
}
_169
_169
// validasi konfigurasi dengan ping ke database server
_169
if err = db.Ping(); err != nil {
_169
log.Fatal(err)
_169
}
_169
log.Println("connected successfully!")
_169
_169
env := &Env{db: db}
_169
http.Handle("/pokemons", pokemonsIndex(env))
_169
http.Handle("/pokemons/show", pokemonsShow(env))
_169
http.Handle("/pokemons/create", pokemonsCreate(env))
_169
http.ListenAndServe(":3000", nil)
_169
}
_169
_169
func pokemonsIndex(env *Env) http.Handler {
_169
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_169
_169
if r.Method != "GET" {
_169
http.Error(w, http.StatusText(405), 405)
_169
return
_169
}
_169
rows, err := env.db.Query("SELECT * FROM pokemons")
_169
if err != nil {
_169
log.Fatal(err)
_169
}
_169
_169
defer rows.Close()
_169
_169
pokemons := make([]*Pokemon, 0)
_169
for rows.Next() {
_169
p := new(Pokemon)
_169
err := rows.Scan(
_169
&p.ID,
_169
&p.Name,
_169
&p.Species,
_169
&p.Height,
_169
&p.Weight,
_169
)
_169
if err != nil {
_169
log.Fatal(err)
_169
}
_169
pokemons = append(pokemons, p)
_169
}
_169
_169
if err = rows.Err(); err != nil {
_169
log.Fatal(err)
_169
}
_169
_169
for _, p := range pokemons {
_169
fmt.Fprintln(w, p.String())
_169
}
_169
})
_169
}
_169
_169
func pokemonsShow(env *Env) http.Handler {
_169
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_169
if r.Method != "GET" {
_169
http.Error(w, http.StatusText(405), 405)
_169
return
_169
}
_169
_169
id, err := strconv.Atoi(r.FormValue("id"))
_169
if id == 0 || err != nil {
_169
http.Error(w, http.StatusText(400), 400)
_169
return
_169
}
_169
_169
row := env.db.QueryRow("SELECT * FROM pokemons WHERE id = $1", id)
_169
_169
p := new(Pokemon)
_169
err = row.Scan(&p.ID, &p.Name, &p.Species, &p.Height, &p.Weight)
_169
if err == sql.ErrNoRows {
_169
http.NotFound(w, r)
_169
return
_169
} else if err != nil {
_169
http.Error(w, http.StatusText(500), 500)
_169
return
_169
}
_169
fmt.Fprintf(w, p.String())
_169
})
_169
}
_169
_169
func pokemonsCreate(env *Env) http.Handler {
_169
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_169
_169
if r.Method != "POST" {
_169
http.Error(w, http.StatusText(405), 405)
_169
return
_169
}
_169
_169
idStr := r.FormValue("id")
_169
name := r.FormValue("name")
_169
species := r.FormValue("species")
_169
heightStr := r.FormValue("height")
_169
weightStr := r.FormValue("weight")
_169
if idStr == "" || name == "" || species == "" || heightStr == "" || weightStr == "" {
_169
http.Error(w, http.StatusText(400), 400)
_169
return
_169
}
_169
id, err := strconv.Atoi(idStr)
_169
if err != nil {
_169
http.Error(w, http.StatusText(400), 400)
_169
return
_169
}
_169
height, err := strconv.ParseFloat(heightStr, 64)
_169
if err != nil {
_169
http.Error(w, http.StatusText(400), 400)
_169
return
_169
}
_169
weight, err := strconv.ParseFloat(weightStr, 64)
_169
if err != nil {
_169
http.Error(w, http.StatusText(400), 400)
_169
return
_169
}
_169
result, err := env.db.Exec("INSERT INTO pokemons VALUES($1, $2, $3, $4, $5)", id, name, species, height, weight)
_169
if err != nil {
_169
fmt.Fprintf(w, "something wrong", err)
_169
http.Error(w, http.StatusText(500), 500)
_169
return
_169
}
_169
_169
rowsAffected, err := result.RowsAffected()
_169
if err != nil {
_169
http.Error(w, http.StatusText(500), 500)
_169
return
_169
}
_169
fmt.Fprintf(w, "Pokemon %d created successfully (%d row affected)\n", id, rowsAffected)
_169
})
_169
}

Sekian dulu pengenalan penggunaan database kali ini, pada tulisan selanjutkan kita akan membuat ReST API yang menggunakan json tapi tetap menggunakan studi kasus pokedex.