Golang SQL Database

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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.

1
2
3
4
$ mkdir pokedex && cd pokedex
$ touch main.go
$ go mod init example.com/pokedex
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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:

1
2
3
4
5
$ 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!

Dasar SQL

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 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:

  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,

1
2
3
4
5
6
7
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:

  • GET /pokemons – daftar semua pokemon di pokedex
  • GET /pokemons/show – menampilkan spesifik pokemon berdasarkan pokemon id
  • POST /pokemons/create – menambahkan pokemon baru ke pokedex
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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.

updatedupdated2020-10-152020-10-15