- Published on
Golang SQL Database
- Authors
- Name
- Moch Lutfi
- @kaptenupi
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_17CREATE 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_17INSERT 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_17ALTER 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_23CREATE 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);_23CREATE TABLE_23INSERT 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);_23INSERT 0 5_23ALTER TABLE pokemons ADD PRIMARY KEY (id);_23ALTER 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_4go: 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_25package main_25_25import (_25 "database/sql"_25 "log"_25_25 // Import the pq driver._25 _ "github.com/lib/pq"_25)_25_25func 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_5go: finding module for package github.com/lib/pq_5go: downloading github.com/lib/pq v1.8.0_5go: found github.com/lib/pq in github.com/lib/pq v1.8.0_519:54:15 connected successfully!
Dasar SQL
Kita mulai dengan query receh SELECT * FROM pokemons
kemudian kita tampilkan kedalam stdout
.
_69// main.go_69package main_69_69import (_69 "database/sql"_69 "fmt"_69 "log"_69_69 // Import the pq driver._69 _ "github.com/lib/pq"_69)_69_69type Pokemon struct {_69 ID int_69 Name string_69 Species string_69 Height float64_69 Weight float64_69}_69_69func (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_69func 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:
- Mengambil resulset dari tabel
pokemons
menggunakan fungsiDB.Query()
dan diletakkan di variabelrows
. Setelah itudefer 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. - Kemudian menggunakan
rows.Next()
untuk iterasi semua baris dalam resultset dan dilanjutkan denganrows.Scan()
untuk memindahkan data. Urutan ketikarows.Scan()
ini sesuai query, dalam hal ini kita menggunakanSELECT *
berarti urutannya sesuai dengan tabel didatabase, jika querynyaSELECT c, b, a
maka urutanya sesuai dengan deklarasi diSELECT
statement yaitu c, b, a. - Ketika
rows.Next()
loop selesai kita panggilrows.Err()
. Ini untuk memastikan jika ada error ketika melakukan iterasi, karena tidak semua iterasi diatas pasti selalu berakhir bahagia tanpa error. - Jika semuanya aman tanpa error maka tinggal loop variabel
pokemons
dan kita tampilkan informasi kedalam stdout.
Tampilan kode diatas sebagai berikut,
_7go run main.go_72020/10/09 20:09:18 connected successfully!_71, Bulbasaur, Seed Pokémon, 0.70 m, 6.90 Kg_72, Ivysaur, Seed Pokémon, 1.00 m, 12.00 Kg_73, Venusaur, Seed Pokémon, 2.00 m, 100.00 Kg_7132, Ditto, Transform Pokémon, 0.30 m, 4.00 Kg_7808, 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
_53package main_53_53import (_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_53type Pokemon struct {_53 ID int_53 Name string_53 Species string_53 Height float64_53 Weight float64_53}_53_53func (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_53type Env struct {_53 db *sql.DB_53}_53_53func 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.
_39func 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
.
_27func 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.
_47func 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
_169package main_169_169import (_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_169type Pokemon struct {_169 ID int_169 Name string_169 Species string_169 Height float64_169 Weight float64_169}_169_169func (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_169type Env struct {_169 db *sql.DB_169}_169_169func 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_169func 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_169func 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_169func 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.