Tutorial ini memperkenalkan dasar-dasar mengakses basis-data relasional dengan Go menggunakan paket database/sql dari pustaka baku.

Anda akan lebih mudah mengikuti tutorial ini bila telah terbiasa dengan perkakas Go. Bila ini adalah pengalaman pertama Anda dengan Go, silahkan lihat Memulai dengan Go untuk pengenalan singkat.

Paket database/sql memiliki beragam tipe dan fungsi untuk terhubung ke basis-data, mengeksekusi transaksi, membatalkan operasi yang sedang berjalan, dan banyak lagi. Untuk rincian tentang penggunaan paket, lihat Mengakses basis-data.

Pada tutorial ini, kita akan membuat sebuah basis-data, dan menulis kode untuk mengakses basis-data. Data yang akan kita gunakan pada proyek contoh ini yaitu album-album musik jazz lama.

Dalam tutorial ini, kita akan melewati tahap-tahap berikut:

  1. Membuat direktori untuk penyimpanan kode.

  2. Menyiapkan sebuah basis-data.

  3. Mengimpor driver untuk basis-data.

  4. Terhubung ke basis-data.

  5. Mengambil banyak baris dari basis-data.

  6. Mengambil sebuah baris dari basis-data.

  7. Menambahkan data.

Note
Untuk tutorial lainnya, lihat halaman Tutorial.

Kebutuhan

  • Pemasangan sistem manajemen basis-data (database management system atau DBMS) PostgreSQL.

  • Pemasangan Go. Untuk instruksi pemasangan lihat Memasang Go.

  • Perkakas untuk menyunting kode.

  • Terminal untuk mengeksekusi perintah. Go bekerja di terminal mana pun di sistem Linux dan Mac dan di PowerShell atau cmd di Windows.

Buat direktori untuk penyimpanan kode

Untuk memulai, buatlah sebuah direktori untuk kode yang akan kita tulis.

  1. Buka terminal dan pindah ke direktori pengguna Anda.

    Pada Linux atau Mac:

    $ cd

    Pada Windows:

    C:\> cd %HOMEPATH%

    Selanjutnya pada tutorial ini kita akan menggunakan $ sebagai tanda perintah terminal. Perintah yang akan tertera bisa berjalan pada Windows juga.

  2. Dari terminal, buatlah sebuah direktori bernama data-access.

    $ mkdir data-access
    $ cd data-access
  3. Buat sebuah modul untuk menyimpan dependensi yang nanti kita tambahkan selama tutorial.

    Jalankan perintah go mod init, dengan parameter nama modul.

    $ go mod init example/data-access
    go: creating new go.mod: module example/data-access

    Perintah ini membuat berkas go.mod tempat dependensi yang nanti kita tambahkan akan tersimpan. Untuk informasi lanjut, lihat Manajemen dependensi.

    Note
    Pada pengembangan sebenarnya, Anda sebaiknya membuat nama modul yang sesuai dengan lingkungan kerja Anda. Lebih lanjut, lihat Manajemen dependensi.

Selanjutnya kita akan membuat sebuah basis-data.

Menyiapkan sebuah basis-data

Pada langkah ini, kita akan membuat basis-data yang akan kita gunakan selama tutorial ini. Kita akan menggunakan antar-muka perintah (command-line interface atau CLI) yang disediakan oleh DBMS untuk membuat basis-data dan tabel, dan juga untuk menambahkan data.

Kita akan membuat sebuah basis-data tentang album-album jazz lama.

Perintah-perintah dalam bagian ini meggunakan PostgreSQL psql, namun kebanyakan DBMS memiliki CLI mereka sendiri dengan fitur-fitur yang mirip.

  1. Buka terminal yang baru.

  2. Masuk ke DBMS, berikut contoh pada PostgreSQL.

    $ psql -U postgres
    postgres=#
  3. Pada baris perintah postgres=#, buat lah sebuah basis-data.

    postgres=# create database recordings;
  4. Pindah lah ke basis-data yang baru kita buat untuk membuat tabel.

    postgres=# \c recordings
    You are now connected to database "recordings" as user "postgres".
    recordings=#
  5. Dalam direktori data-access, buatlah sebuah berkas bernama create-tables.sql yang akan menyimpan skrip SQL untuk membuat tabel-tabel.

  6. Di dalam berkas tersebut, tempel kode SQL berikut, kemudian simpan berkas.

    DROP TABLE IF EXISTS album;
    CREATE TABLE album (
      id         SERIAL,
      title      VARCHAR(128) NOT NULL,
      artist     VARCHAR(255) NOT NULL,
      price      DECIMAL(5,2) NOT NULL,
      PRIMARY KEY (`id`)
    );
    
    INSERT INTO album
      (title, artist, price)
    VALUES
      ('Blue Train', 'John Coltrane', 56.99),
      ('Giant Steps', 'John Coltrane', 63.99),
      ('Jeru', 'Gerry Mulligan', 17.99),
      ('Sarah Vaughan', 'Sarah Vaughan', 34.98);

    Dalam kode SQL ini, kita:

    • Menghapus (drop) sebuah tabel bernama album. Mengeksekusi perintah ini terlebih dahulu membuat kita lebih mudah menjalankan ulang skrip nantinya, seandainya kita akan ulang lagi dari awal.

    • Membuat sebuah tabel album dengan empat kolom: id, title, artist, dan price. Setiap nilai kolom id diisi oleh DBMS secara otomatis.

    • Menambahkan empat baris data ke dalam tabel album.

  7. Dari terminal psql, jalankan skrip yang baru kita buat tersebut.

    Kita gunakan perintah \i dengan cara berikut:

    recordings=# \i create-tables.sql
  8. Lewat terminal psql, gunakan perintah SELECT untuk memeriksa bahwa tabel telah terbuat dan berisi data.

    recordings=# select * from album;
     id |     title     |     artist     | price
    ----+---------------+----------------+-------
      1 | Blue Train    | John Coltrane  | 56.99
      2 | Giant Steps   | John Coltrane  | 63.99
      3 | Jeru          | Gerry Mulligan | 17.99
      4 | Sarah Vaughan | Sarah Vaughan  | 34.98
    (4 rows)

Selanjutnya, kita akan menulis kode Go untuk terhubung ke basis-data dan membaca data dari dalam tabel.

Mengimpor driver untuk basis-data

Setelah kita memiliki sebuah basis-data yang berisi sebuah tabel dan data, saatnya mulai menulis kode Go.

Untuk itu kita membutuhkan sebuah driver basis-data yang akan menerjemahkan permintaan yang kita buat lewat fungsi-fungsi dalam paket database/sql menjadi permintaan yang dapat dipahami oleh basis-data.

  1. Lewat peramban, kunjungi halaman wiki SQLDrivers untuk menentukan driver yang akan kita gunakan.

    Gunakan daftar di halaman tersebut untuk menentukan driver yang akan kita gunakan. Untuk mengakses PostgreSQL dalam tutorial ini, kita akan menggunakan modul github.com/lib/pq

  2. Catat nama modul dari driver  — yaitu, github.com/lib/pq.

  3. Buat sebuah berkas main.go untuk menulis kode Go yang disimpan dalam direktori data-access.

  4. Dalam main.go, tempel kode berikut untuk mengimpor driver.

    package main
    
    import "github.com/lib/pq"

    Dalam kode ini, kita:

    • Menambahkan berkas kode tersebut ke dalam paket main sehingga kita dapat mengeksekusi-nya nanti.

    • Mengimpor driver PostgreSQL github.com/lib/pq.

Setelah mengimpor driver, kita akan memulai menulis kode untuk mengakses basis-data.

Terhubung ke basis-data

Sekarang tulis kode Go yang menghubungkan Anda ke basis-data.

Kita akan menggunakan pointer ke struct sql.DB, yang merepresentasikan akses ke basis-data.

  1. Dalam main.go di bawah kode import yang kita tambahkan sebelumnya, tempel kode Go berikut untuk membuat koneksi ke basis-data.

    func main() {
    	// Contoh DATABASE_URL = "postgres://username:password@localhost:5432/database_name"
    	databaseUrl := os.Getenv("DATABASE_URL")
    	var connector *pq.Connector
    	var err error
    	connector, err = pq.NewConnector(databaseUrl)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	var db *sql.DB
    	db = sql.OpenDB(connector)
    	defer db.Close()
    
    	err = db.Ping()
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Println("Terhubung!")
        }

    Dalam kode tersebut, kita:

    • Menggunakan pq.NewConnector untuk membuat penghubung yang menerima alamat basis-data.

    • Memeriksa kegagalan dari pq.NewConnector. Fungsi ini bisa gagal bila alamat koneksi salah. Untuk mempermudah kode, kita gunakan log.Fatal untuk mengakhiri eksekusi dan mencetak galat ke layar.

    • Memanggil sql.OpenDB untuk menginisialiasi variabel db dengan mengirim nilai kembalian dari NewConnector.

    • Memanggil DB.Ping untuk memastikan bahwa koneksi ke basis-data bekerja. Pada saat program dijalankan, pemanggilan sql.OpenDB bisa jadi tidak langsung terhubung ke basis-data, bergantung kepada driver yang digunakan.

    • Memeriksa galat dari Ping, bila koneksi gagal.

    • Mencetak pesan bila Ping terhubung dengan sukses.

  2. Di bagian atas berkas main.go, di bawah deklarasi paket, impor paket-paket yang kita butuhkan untuk mendukung kode yang telah kita tulis.

    Bagian atas dari berkas seharusnya seperti berikut:

    package main
    
    import (
    	"database/sql"
    	"fmt"
    	"log"
    	"os"
    
    	"github.com/lib/pq"
    )
  3. Simpan berkas main.go

Jalankan kode

  1. Tambahkan modul driver PostgreSQL sebagai dependensi.

    Gunakan perintah go get untuk menambahkan modul "github.com/lib/pq" sebagai dependensi ke dalam modul kita. Gunakan argumen titik yang artinya "ambil semua dependensi kode dalam direktori ini".

    $ go get .
    go: added github.com/lib/pq v1.10.9

    Go mengunduh dependensi tersebut karena kita menambahkannya ke dalam deklarasi import pada langkah sebelumnya. Untuk informasi lanjut tentang pelacakan dependensi, lihat Menambahkan sebuah dependensi.

  2. Dari terminal, set variabel lingkungan DATABASE_URL untuk digunakan pada program.

    Pada Linux atau Mac:

    $ export DATABASE_URL=postgresql://postgres@127.0.0.1/recordings?sslmode=disable

    Pada Windows:

    C:\> set DATABASE_URL=postgresql://postgres@127.0.0.1/recordings?sslmode=disable
  3. Dari dalam direktori yang berisi main.go, jalankan kode dengan perintah go run dengan argumen titik yang artinya "jalankan paket main di dalam direktori ini".

    $ go run .
    Terhubung!

Kita telah terhubung! Selanjutnya, kita akan mengambil beberapa baris dari basis-data.

Mengambil banyak baris

Pada bab ini, kita akan gunakan Go untuk mengeksekusi kueri SQL untuk mengembalikan banyak baris.

Untuk perintah SQL yang mengembalikan banyak baris, kita gunakan method Query dari paket database/sql, kemudian mengiterasi baris-baris yang dikembalikan. (Kita akan belajar mengambil sebuah baris nantinya, dalam bab Mengambil sebuah baris).

  1. Dalam main.go, sebelum func main, tempel definisi struct Album berikut. Kita akan menggunakan struct ini untuk menyimpan data yang dikembalikan dari kueri.

    type Album struct {
    	ID     int64
    	Title  string
    	Artist string
    	Price  float32
    }
  2. Sebelum func main, tempel fungsi albumsByArtist untuk kueri ke basis-data.

    // albumsByArtist mengambil album-album berdasarkan nama artis.
    func albumsByArtist(db *sql.DB, name string) ([]Album, error) {
    	// albums adalah slice yang menyimpan data dari hasil kueri.
    	var albums []Album
    
    	rows, err := db.Query(`
    		SELECT id, title, artist, price
    		FROM album
    		WHERE artist = $1`, name)
    	if err != nil {
    		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    	}
    	defer rows.Close()
    
    	// Iterasi pada rows, menggunakan Scan untuk menyimpan data ke dalam
    	// struct Album.
    	for rows.Next() {
    		var alb Album
    		err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price)
    		if err != nil {
    			return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    		}
    		albums = append(albums, alb)
    	}
    	if err := rows.Err(); err != nil {
    		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    	}
    	return albums, nil
    }

    Dalam kode tersebut, kita:

    • Mendeklarasikan sebuah slice albums bertipe Album. Slice ini akan menyimpan baris-baris data kembalian dari basis-data. Field-field pada struct berkorespondensi dengan nama dan tipe kolum pada basis-data.

    • Menggunakan DB.Query yang mengeksekusi perintah SELECT untuk mengueri album berdasarkan nama artis.

      Parameter pertama dari Query yaitu perintah SQL. Setelah perintah SQL, kita bisa mengirim nol atau lebih parameter dengan tipe apa pun, sebagai nilai dari parameter dalam perintah SQL. Dengan memisahkan perintah SQL dari nilai parameter (bukan dengan menggabungkannya dengan, katakan lah, fmt.Sprintf), kita membuat paket database/sql mengirim nilai terpisah dari teks SQL, menghindari resiko injeksi SQL.

    • Menunda penutupan rows sampai fungsi keluar, supaya sumber yang terpakai dapat dirilis kembali ke sistem.

    • Iterasi kembalian rows, menggunakan Rows.Scan untuk mengisi setiap nilai baris kolom ke dalam field struct Album.

      Scan menerima pointer ke variabel, tempat nilai kolom akan ditulis. Di sini kita mengirim pointer ke variabel alb, dibuat menggunakan operator &. Scan menulis lewat pointer untuk mengisi field pada struct.

    • Di dalam iterasi, periksa kesalahan saat mengonversi nilai kolom ke dalam field-field struct.

    • Di dalam iterasi, tambahkan nilai alb yang baru ke dalam slice album.

    • Setelah iterasi, periksa galat dari semua kueri, menggunakan rows.Err. Jika kueri gagal, satu-satunya cara memeriksa galat untuk mengetahui apakah berhasil atau tidak hanyalah di sini.

  3. Perbarui fungsi main supaya memanggil albumsByArtist.

    Di akhir func main, tambahkan kode berikut.

    albums, err := albumsByArtist("John Coltrane")
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Printf("Albums ditemukan: %v\n", albums)

    Dalam kode di atas, kita:

    • Memanggil fungsi albumsByArtist yang ditambahkan sebelumnya, menyimpan nilai kembalian ke variabel albums.

    • Mencetak hasil.

Menjalankan kode

Dari terminal, dalam direktori yang berisi main.go, jalankan kode.

$ go run .
Terhubung!
Albums ditemukan: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]

Selanjutnya, kita akan mengueri satu baris data.

Kueri satu baris data

Pada bab ini, kita akan mengueri sebuah baris dalam basis-data.

Untuk perintah SQL yang mengembalikan sebuah baris, kita dapat menggunakan QueryRow, yang lebih mudah daripada Query.

  1. Di bawah albumsByArtist, tempel fungsi albumByID berikut,

    // albumByID kueri album berdasarkan ID.
    func albumByID(db *sql.DB, id int64) (Album, error) {
    	// Variabel yang menyimpan baris kembalian dari basis-data.
    	var alb Album
    
    	row := db.QueryRow(`
    		SELECT id, title, artist, price
    		FROM album WHERE id = $1", id)
    	err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price)
    	if err != nil {
    		if err == sql.ErrNoRows {
    			return alb, fmt.Errorf("albumsById %d: album tidak ditemukan", id)
    		}
    		return alb, fmt.Errorf("albumsById %d: %v", id, err)
    	}
    	return alb, nil
    }

    Dalam kode tersebut, kita:

    • Menggunakan DB.QueryRow untuk mengeksekusi perintah SELECT untuk mengueri sebuah album dengan ID tertentu.

      Fungsi QueryRow mengembalikan sql.Row. Fungsi QueryRow tidak mengembalikan sebuah error. Namun, ia akan mengembalikan sebuah error nanti saat Row.Scan dipanggil.

    • Menggunakan Row.Scan untuk menyalin nilai kolom ke dalam field-field pada struct.

    • Memeriksa galat dari Scan.

      Galat sql.ErrNoRows mengindikasikan bahwa kueri tidak mengembalikan baris. Biasanya, galat tersebut diganti dengan teks yang lebih berarti, seperti "album tidak ditemukan".

  2. Perbarui main supaya memanggil albumByID.

    Pada akhir func main, tambahkan kode berikut.

    // Tulis langsung ID 2 untuk menguji kueri.
    alb, err := albumByID(2)
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Printf("Album ditemukan: %v\n", alb)

    Pada kode di atas, kita:

    • Memanggil fungsi albumByID yang baru ditambahkan.

    • Mencetak album yang dikembalikan.

Menjalankan kode

Dari terminal, dalam direktori yang berisikan main.go, jalankan kode.

$ go run .
Terhubung!
Albums ditemukan: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album ditemukan: {2 Giant Steps John Coltrane 63.99}

Selanjutnya, kita akan menambahkan sebuah album ke dalam basis-data.

Menambahkan data

Pada bab ini, kita akan mengeksekusi perintah SQL INSERT untuk menambahkan sebuah baris baru pada basis-data.

Kita telah melihat cara menggunakan perintah SQL Query dan QueryRow untuk mengambil data. Untuk mengeksekusi perintah SQL yang tidak mengembalikan data, kita gunakan Exec. Khusus pada PostgreSQL, bila kita ingin mengambil ID yang baru dari hasil INSERT kita tetap harus menggunakan QueryRow diikuti dengan Scan.

  1. Di bawah albumByID, tempel fungsi addAlbum berikut untuk mengisi sebuah album baru ke basis-data, kemudian simpan main.go.

    // addAlbum menambahkan sebuah album baru ke dalam basis-data dan
    // mengembalikan ID album yang baru.
    func addAlbum(db *sql.DB, alb Album) (int64, error) {
    	var id int64
    	err := db.QueryRow(`
    		INSERT INTO album (title, artist, price)
    		VALUES ($1, $2, $3)
    		RETURNING id`,
    		alb.Title, alb.Artist, alb.Price).Scan(&id)
    	if err != nil {
    		return 0, fmt.Errorf("addAlbum: %v", err)
    	}
    	return id, nil
    }

    Dalam kode ini, kita:

    • Menggunakan DB.QueryRow untuk mengeksekusi perintah INSERT.

    • Menerima ID dari baris yang baru diisi ke basis-data menggunakan Row.Scan

    • Memeriksa galat dari pengambilan ID

  2. Perbarui main supaya memanggil fungsi addAlbum.

    Pada akhir func main, tambah kode berikut.

    albID, err := addAlbum(db, Album{
    	Title:  "The Modern Sound of Betty Carter",
    	Artist: "Betty Carter",
    	Price:  49.99,
    })
    if err != nil {
    	log.Fatal(err)
    }
    fmt.Printf("ID dari album baru: %v\n", albID)

    Pada kode yang baru ini, kita:

    • Memanggil addAlbum dengan sebuah album baru, dan menerima ID dari album yang baru ke variabel albID.

Jalankan kode

Dari terminal, di dalam direktori yang berisi main.go, jalankan kode.

$ go run .
Terhubung!
Albums ditemukan: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album ditemukan: {2 Giant Steps John Coltrane 63.99}
ID dari album baru: 5

Kesimpulan

Selamat! Kita baru saja menggunakan Go untuk melakukan aksi-aksi sederhana dengan basis-data relasional.

Rekomendasi topik-topik selanjutnya:

  • Lihatlah panduan data akses, yang mengikutkan informasi lanjut tentang subjek-subjek yang kita pelajari di sini.

  • Jika Anda baru belajar Go, Anda akan menemukan praktik terbaik dijelaskan dalam Efektif Go dan Menulis kode Go.

  • Tur Go adalah pengenalan langkah demi langkah dari fundamental Go.

Kode lengkap

Bab ini berisi kode lengkap untuk aplikasi yang telah kita buat selama tutorial ini.

package main

import (
	"database/sql"
	"fmt"
	"log"
	"os"

	"github.com/lib/pq"
)

type Album struct {
	ID     int64
	Title  string
	Artist string
	Price  float32
}

// albumsByArtist mengambil album-album berdasarkan nama artis.
func albumsByArtist(db *sql.DB, name string) ([]Album, error) {
	// albums adalah slice yang menyimpan data dari hasil kueri.
	var albums []Album

	rows, err := db.Query(`
		SELECT id, title, artist, price
		FROM album
		WHERE artist = $1`, name)
	if err != nil {
		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
	}
	defer rows.Close()

	// Iterasi pada rows, menggunakan Scan untuk menyimpan data ke dalam
	// struct Album.
	for rows.Next() {
		var alb Album
		err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price)
		if err != nil {
			return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
		}
		albums = append(albums, alb)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
	}
	return albums, nil
}

// albumByID kueri album berdasarkan ID.
func albumByID(db *sql.DB, id int64) (Album, error) {
	// Variabel yang menyimpan baris kembalian dari basis-data.
	var alb Album

	row := db.QueryRow(`
		SELECT id, title, artist, price
		FROM album WHERE id = $1", id)
	err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price)
	if err != nil {
		if err == sql.ErrNoRows {
			return alb, fmt.Errorf("albumsById %d: album tidak ditemukan", id)
		}
		return alb, fmt.Errorf("albumsById %d: %v", id, err)
	}
	return alb, nil
}

// addAlbum menambahkan sebuah album baru ke dalam basis-data dan
// mengembalikan ID album yang baru.
func addAlbum(db *sql.DB, alb Album) (int64, error) {
	var id int64
	err := db.QueryRow(`
		INSERT INTO album (title, artist, price)
		VALUES ($1, $2, $3)
		RETURNING id`,
		alb.Title, alb.Artist, alb.Price).Scan(&id)
	if err != nil {
		return 0, fmt.Errorf("addAlbum: %v", err)
	}
	return id, nil
}

func main() {
	// Contoh DATABASE_URL = "postgres://username:password@localhost:5432/database_name"
	databaseUrl := os.Getenv("DATABASE_URL")
	var connector *pq.Connector
	var err error
	connector, err = pq.NewConnector(databaseUrl)
	if err != nil {
		log.Fatal(err)
	}

	var db *sql.DB
	db = sql.OpenDB(connector)
	defer db.Close()

	err = db.Ping()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Terhubung!")

	albums, err := albumsByArtist(db, "John Coltrane")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Albums ditemukan: %v\n", albums)

	// Tulis langsung ID 2 untuk menguji kueri.
	alb, err := albumByID(db, 2)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Album ditemukan: %v\n", alb)

	albID, err := addAlbum(db, Album{
		Title:  "The Modern Sound of Betty Carter",
		Artist: "Betty Carter",
		Price:  49.99,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("ID dari album baru: %v\n", albID)
}