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
.
Pertama-tama kita perlu membuat database pokedex
dengan script sql sebagai berikut:
-- migration.sql
CREATE TABLE pokemons (
id int NOT NULL,
name varchar(255) NOT NULL,
species varchar(255) NOT NULL,
height decimal(5,2) NOT NULL,
weight decimal(5,2) NOT NULL
);
INSERT INTO pokemons (id, name, species, height, weight) VALUES
(1, 'Bulbasaur', 'Seed Pokémon', 0.7, 6.9),
(2, 'Ivysaur', 'Seed Pokémon', 1, 12),
(3, 'Venusaur', 'Seed Pokémon', 2, 100),
(132, 'Ditto', 'Transform Pokémon', 0.3, 4),
(808, 'Meltan', 'Hex Nut Pokémon', 0.2, 8);
ALTER TABLE pokemons ADD PRIMARY KEY (id);
Kemudian tinggal buat database dan jalankan migration.sql
diatas
# buat database pokedex
$ psql -h localhost -Upostgres -W -c "CREATE DATABASE pokedex";
# import migration sql ke database pokedex
$ psql -h localhost -Upostgres -W -d pokedex -a -f migration.sql
-- migration.ssql
CREATE TABLE pokemons (
id int NOT NULL,
name varchar(255) NOT NULL,
species varchar(255) NOT NULL,
height decimal(5,2) NOT NULL,
weight decimal(5,2) NOT NULL
);
CREATE TABLE
INSERT INTO pokemons (id, name, species, height, weight) VALUES
(1, 'Bulbasaur', 'Seed Pokémon', 0.7, 6.9),
(2, 'Ivysaur', 'Seed Pokémon', 1, 12),
(3, 'Venusaur', 'Seed Pokémon', 2, 100),
(132, 'Ditto', 'Transform Pokémon', 0.3, 4),
(808, 'Meltan', 'Hex Nut Pokémon', 0.2, 8);
INSERT 0 5
ALTER TABLE pokemons ADD PRIMARY KEY (id);
ALTER TABLE
Setelah persiapan database selesai sekarang dilanjutkan dengan bootstrap code.
$ mkdir pokedex && cd pokedex
$ touch main.go
$ go mod init example.com/pokedex
go: creating new go.mod: module example.com/pokedex
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
// main.go
package main
import (
"database/sql"
"log"
// Import the pq driver.
_ "github.com/lib/pq"
)
func main() {
// init koneksi database instance, tetapi masih belum konek ke database server
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// validasi konfigurasi dengan ping ke database server
if err = db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("connected successfully!")
}
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:
$ go run main.go
go: finding module for package github.com/lib/pq
go: downloading github.com/lib/pq v1.8.0
go: found github.com/lib/pq in github.com/lib/pq v1.8.0
19:54:15 connected successfully!
Kita mulai dengan query receh SELECT * FROM pokemons
kemudian kita tampilkan kedalam stdout
.
// main.go
package main
import (
"database/sql"
"fmt"
"log"
// Import the pq driver.
_ "github.com/lib/pq"
)
type Pokemon struct {
ID int
Name string
Species string
Height float64
Weight float64
}
func (p Pokemon) String() string {
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg", p.ID, p.Name, p.Species, p.Height, p.Weight)
}
func main() {
// init koneksi database instance, tetapi masih belum konek ke database server
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// validasi konfigurasi dengan ping ke database server
if err = db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("connected successfully!")
rows, err := db.Query("SELECT * FROM pokemons")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
pokemons := make([]*Pokemon, 0)
for rows.Next() {
p := new(Pokemon)
err := rows.Scan(
&p.ID,
&p.Name,
&p.Species,
&p.Height,
&p.Weight,
)
if err != nil {
log.Fatal(err)
}
pokemons = append(pokemons, p)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
for _, p := range pokemons {
fmt.Println(p.String())
}
}
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:
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.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.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.pokemons
dan kita tampilkan informasi kedalam stdout.Tampilan kode diatas sebagai berikut,
go run main.go
2020/10/09 20:09:18 connected successfully!
1, Bulbasaur, Seed Pokémon, 0.70 m, 6.90 Kg
2, Ivysaur, Seed Pokémon, 1.00 m, 12.00 Kg
3, Venusaur, Seed Pokémon, 2.00 m, 100.00 Kg
132, Ditto, Transform Pokémon, 0.30 m, 4.00 Kg
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:
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
// Import the pq driver.
_ "github.com/lib/pq"
)
type Pokemon struct {
ID int
Name string
Species string
Height float64
Weight float64
}
func (p Pokemon) String() string {
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg",
p.ID,
p.Name,
p.Species,
p.Height,
p.Weight)
}
type Env struct {
db *sql.DB
}
func main() {
// init koneksi database instance, tetapi masih belum konek ke database server
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// validasi konfigurasi dengan ping ke database server
if err = db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("connected successfully!")
env := &Env{db: db}
http.Handle("/pokemons", pokemonsIndex(env))
http.Handle("/pokemons/show", pokemonsShow(env))
http.Handle("/pokemons/create", pokemonsCreate(env))
http.ListenAndServe(":3000", nil)
}
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.
func pokemonsIndex(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
rows, err := env.db.Query("SELECT * FROM pokemons")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
pokemons := make([]*Pokemon, 0)
for rows.Next() {
p := new(Pokemon)
err := rows.Scan(
&p.ID,
&p.Name,
&p.Species,
&p.Height,
&p.Weight,
)
if err != nil {
log.Fatal(err)
}
pokemons = append(pokemons, p)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
for _, p := range pokemons {
fmt.Fprintln(w, p.String())
}
})
}
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
.
func pokemonsShow(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
id, err := strconv.Atoi(r.FormValue("id"))
if id == 0 || err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
row := env.db.QueryRow("SELECT * FROM pokemons WHERE id = $1", id)
p := new(Pokemon)
err = row.Scan(&p.ID, &p.Name, &p.Species, &p.Height, &p.Weight)
if err == sql.ErrNoRows {
http.NotFound(w, r)
return
} else if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
fmt.Fprintf(w, p.String())
})
}
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.
func pokemonsCreate(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, http.StatusText(405), 405)
return
}
idStr := r.FormValue("id")
name := r.FormValue("name")
species := r.FormValue("species")
heightStr := r.FormValue("height")
weightStr := r.FormValue("weight")
if idStr == "" || name == "" || species == "" || heightStr == "" || weightStr == "" {
http.Error(w, http.StatusText(400), 400)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
height, err := strconv.ParseFloat(heightStr, 64)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
weight, err := strconv.ParseFloat(weightStr, 64)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
result, err := env.db.Exec("INSERT INTO pokemons VALUES($1, $2, $3, $4, $5)", id, name, species, height, weight)
if err != nil {
fmt.Fprintf(w, "something wrong", err)
http.Error(w, http.StatusText(500), 500)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
fmt.Fprintf(w, "Pokemon %d created successfully (%d row affected)\n", id, rowsAffected)
})
}
Jika semua digabungkan code diatas maka hasilnya seperti ini
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
// Import the pq driver.
_ "github.com/lib/pq"
)
type Pokemon struct {
ID int
Name string
Species string
Height float64
Weight float64
}
func (p Pokemon) String() string {
return fmt.Sprintf("%d, %s, %s, %.2f m, %.2f Kg",
p.ID,
p.Name,
p.Species,
p.Height,
p.Weight)
}
type Env struct {
db *sql.DB
}
func main() {
// init koneksi database instance, tetapi masih belum konek ke database server
db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/pokedex?sslmode=disable")
if err != nil {
log.Fatal(err)
}
// validasi konfigurasi dengan ping ke database server
if err = db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("connected successfully!")
env := &Env{db: db}
http.Handle("/pokemons", pokemonsIndex(env))
http.Handle("/pokemons/show", pokemonsShow(env))
http.Handle("/pokemons/create", pokemonsCreate(env))
http.ListenAndServe(":3000", nil)
}
func pokemonsIndex(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
rows, err := env.db.Query("SELECT * FROM pokemons")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
pokemons := make([]*Pokemon, 0)
for rows.Next() {
p := new(Pokemon)
err := rows.Scan(
&p.ID,
&p.Name,
&p.Species,
&p.Height,
&p.Weight,
)
if err != nil {
log.Fatal(err)
}
pokemons = append(pokemons, p)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
for _, p := range pokemons {
fmt.Fprintln(w, p.String())
}
})
}
func pokemonsShow(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, http.StatusText(405), 405)
return
}
id, err := strconv.Atoi(r.FormValue("id"))
if id == 0 || err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
row := env.db.QueryRow("SELECT * FROM pokemons WHERE id = $1", id)
p := new(Pokemon)
err = row.Scan(&p.ID, &p.Name, &p.Species, &p.Height, &p.Weight)
if err == sql.ErrNoRows {
http.NotFound(w, r)
return
} else if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
fmt.Fprintf(w, p.String())
})
}
func pokemonsCreate(env *Env) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, http.StatusText(405), 405)
return
}
idStr := r.FormValue("id")
name := r.FormValue("name")
species := r.FormValue("species")
heightStr := r.FormValue("height")
weightStr := r.FormValue("weight")
if idStr == "" || name == "" || species == "" || heightStr == "" || weightStr == "" {
http.Error(w, http.StatusText(400), 400)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
height, err := strconv.ParseFloat(heightStr, 64)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
weight, err := strconv.ParseFloat(weightStr, 64)
if err != nil {
http.Error(w, http.StatusText(400), 400)
return
}
result, err := env.db.Exec("INSERT INTO pokemons VALUES($1, $2, $3, $4, $5)", id, name, species, height, weight)
if err != nil {
fmt.Fprintf(w, "something wrong", err)
http.Error(w, http.StatusText(500), 500)
return
}
rowsAffected, err := result.RowsAffected()
if err != nil {
http.Error(w, http.StatusText(500), 500)
return
}
fmt.Fprintf(w, "Pokemon %d created successfully (%d row affected)\n", id, rowsAffected)
})
}
Sekian dulu pengenalan penggunaan database kali ini, pada tulisan selanjutkan kita akan membuat ReST API yang menggunakan json
tapi tetap menggunakan studi kasus pokedex.