Pemrograman

Cara Koneksi Golang Dengan Database Postgres

Dependency

Pastikan sudah buat projek dengan go mod init postgres-go pada folder postgres-go dan dependency yang akan kita pakai yaitu menggunakan

import _ "github.com/lib/pq"

Jika belum ada bisa kita coba download terlebih dahulu dependency diatas dengan cara perintah dibawah ini.

go get github.com/lib/pg

Buat Fungsi Koneksi Database

Pada pembuatan fungsi koneksi database ini kita akan menggunakan fungsi dari sql yang mana sudah disiapkan oleh library go. Mari kita buat fungsi koneksi database ini dengan mengembalikan return db connection.

func openDB(dsn string, setLimits bool) (*sql.DB, error) {
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		return nil, err
	}

	if setLimits {
		fmt.Println("setting limits")
		db.SetMaxOpenConns(5)
		db.SetMaxIdleConns(5)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	err = db.PingContext(ctx)
	if err != nil {
		return nil, err
	}

	return db, nil
}

Pada fungsi koneksi ini kita menggunakan sql.Open untuk melakukan koneksi database Postgres yang mana kita juga menerima parameter dari fungsi utama itu dsn yang berisi server lokasi, user, password dan database yang dituju.

db, err := sql.Open("postgres", dsn)

Lalu kita juga memberikan beberapa konfigurasi untuk memastikan koneksi ini memiliki SetMaxOpenConns Koneksi saat kita jalankan service ini dan juga SetMaxIdleConns kita set untuk memastikan koneksi kita tidak salah konfigurasi dan berjalan dengan normal.

db.SetMaxOpenConns(5)
db.SetMaxIdleConns(5)

Selanjutnya kita pastikan juga buat context set limit timeout yang mana disini berguna untuk memastikan koneksi ke database ini tidak terlalu lama. Dalam hal ini kita set 5 second batas maksimal timeout-nya.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

Dan terakhir yaitu kita melakukan PingContext ini memastikan bahwa koneksi ke dalam database ini tidak terhambat oleh apapun misalkan delay atau network.

err = db.PingContext(ctx)

Membuat Service and Query ke Database

Pada tahapan ini, kita akan membuat beberapa fungsi untuk mengakses data dari database fungsinya pun kita buat dengan enkapsulasi interface. Mari kita langsung jalankan dan praktikan. Buat folder services lalu kita buat juga file didalam folder tersebut dengan nama file contract.go.

package services

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

type AlbumService interface {
	Get(id int64) (*Album, error)
	Create(album *Album) error
	GetAllAlbum() ([]Album, error)
	BatchCreate(albums []Album) error
	Update(Album Album) error
	Delete(id int64) error
}

Bisa kita lihat bahwa file contract tersebut berisi tentang data struct yang mana akan digunakan untuk dasar struktur datanya album.

Sedangkan fungsi-fungsi yang akan kita gunakan yaitu kebutuhan dari tambah, ubah, hapus. Berikut ini kita buat interface yanng nantinya akan kita implementasi juga pada fungsi-fungsi tersebut.

type AlbumService interface {
	Get(id int64) (*Album, error)
	Create(album *Album) error
	GetAllAlbum() ([]Album, error)
	BatchCreate(albums []Album) error
	Update(Album Album) error
	Delete(id int64) error
}

Mengambil Data Album by Id

Mari kita buat fungsi yang nantinya bisa mengambil data dari database postgres. Sebelum ke fungsi yang terkait kita perlu definisikan terlebih dahulu fungsi-fungsi yang ada dengan menyertakan koneksi database yang sudah kita definisikan di awal.

type PostgresService struct {
	db *sql.DB
}

func NewPostgresService(db *sql.DB) *PostgresService {
	return &PostgresService{db: db}
}

Inisialisasi ini kita gunakan agar bisa kita membuat fungsi yang mengambil data ke dalam database itu dengan koneksi yang sudah ada. Lalu selanjutnya mari kita buat fungsi dari mengambil data album mengunakan parameter id.

func (p *PostgresService) Get(id int64) (*Album, error) {
	query := `
        SELECT id, title, artist, price
        FROM public.album
        WHERE id = $1`

	var album Album

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	err := p.db.QueryRowContext(ctx, query, id).Scan(
		&album.ID,
		&album.Title,
		&album.Artist,
		&album.Price,
	)

	if err != nil {
		return nil, err
	}

	return &album, nil
}

Pada fungsi diatas itu kita melihat buat query ke dalam database seperti dibawah ini.

query := `
        SELECT id, title, artist, price
        FROM public.album
        WHERE id = $1`

Lalu kita juga pastikan bahwa query tersebut perlu timeout agar tidak terlalu lama mengambil data ke dalam database. Pada program ini kita menggunakan timeout 15 second.

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

Selanjutnya kita juga menggunakan perintah QueryRowContext untuk mengambil datanya setelah kita inisialisasi koneksi ke database.

err := p.db.QueryRowContext(ctx, query, id).Scan(
		&album.ID,
		&album.Title,
		&album.Artist,
		&album.Price,
	)

Perintah QueryRowContext ini digunaakn untuk mengambil data dari database dengan mengembalikan hanya satu baris data saja. Selanjutnya jika datanya ada maka akan kita simpan data tersebut ke dalam variabel album.

Menyimpan Data Album ke Database

Selain kita mengambil data dari database, kita juga harus bisa mengirim data dari sistem ke dalam database. Berikut ini kita akan membuat fungsi create.

func (p *PostgresService) Create(album *Album) error {
	query := `
        INSERT INTO public.album (id, title, artist, price) 
        VALUES ($1, $2, $3, $4)
        RETURNING id`

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	return p.db.QueryRowContext(ctx, query, album.Title, album.Artist, album.Price).Scan(&album.ID)
}

Seperti biasa karena ini termasuk ke dalam native query yang mana kita menentukan query sendiri didalam kode maka kita perlu definisikan terlebih dahulu query-nya. Berikut ini query untuk insert data ke dalam database.

query := `
  INSERT INTO public.album (id, title, artist, price) 
  VALUES ($1, $2, $3, $4)
  RETURNING id`

Lakukan set timeout agar lebih terjaga dan selanjutnya kita execute create disini kita menggunakan query sama seperti mengambil data karena kita akan mengambil id setelah ditambahkan.

return p.db.QueryRowContext(ctx, query, album.Title, album.Artist, album.Price).Scan(&album.ID)

Mengambil Semua Data dalam Database

Mari kita buat fungsi selanjutnya yaitu mengambil semua data dari database sebagai berikut.

func (p *PostgresService) GetAllAlbum() ([]Album, error) {
	query := `
		SELECT id, title, artist, price
		FROM album`

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	var albums []Album

	rows, err := p.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var album Album
		err := rows.Scan(
			&album.ID,
			&album.Title,
			&album.Artist,
			&album.Price,
		)
		if err != nil {
			return nil, err
		}

		albums = append(albums, album)
	}

	return albums, nil
}

Pada fungsi ini kita akan mengambil semua datanya dan seperti kita lihat fungsi query yang digunakan berbeda dengan query sebelumnya, yaitu kita menggunakan query QueryContext yang mana hasil dari kembalian fungsi ini memiliki multi rows sehingga kita perlu melakukan perulangan untuk menyimpan datanya ke dalam array struct.

	rows, err := p.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

Berikut ini QueryContext digunakan untuk mengambil data dan jangan lupa karena kembalian ini memiliki multiple rows maka kita perlu juga dilakukan close connection tiap rows-nya diakhir fungsi. Kalau disini kita pakai defer agar dijalankan setelah kembali ke fungsi utama.

defer rows.Close()

Selanjutnya kita akan mengambil data dari rows untuk dipindahkan ke dalam satu struct array.

for rows.Next() {
  var album Album
  err := rows.Scan(
    &album.ID,
    &album.Title,
    &album.Artist,
    &album.Price,
  )
  if err != nil {
    return nil, err
  }

  albums = append(albums, album)
}

Dan diakhiri dengan perintah return albums, nil pada fungsinya.

Membuat Batch Tambah Album

Pada fungsi ini kita akan membuat batching data yang mana kia mengirim data dengan jumlah lebih dari satu. Ini bisa kita gunakan untuk data-data yang kita kirim sekaligus. Mari kita lihat fungsinya yang akan kita buat.

func (p *PostgresService) BatchCreate(albums []Album) error {
	tx, err := p.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `INSERT INTO album (title, artist, price) VALUES ($1, $2, $3)`

	for _, album := range albums {
		_, err := tx.ExecContext(ctx, query, album.Title, album.Artist, album.Price)
		if err != nil {
			log.Printf("error execute insert err: %v", err)
			continue
		}
	}

	err = tx.Commit()
	if err != nil {
		return err
	}

	return nil
}

Fungsi batch ini kita menggunakan beberapa perintah batching transaksi ke dalam database. Pada tahapan awal kita akan membuka transaksi ke db dengan perintah dibawah ini.

tx, err := p.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

Fungsi Begin() digunakan untuk membuka transaksi dan diakhiri dengan defer tx.Rollback() jika di tengah-tengah terjadi sesuatu yang mengakibatkan error.

Selanjutnya kita lakukan perulangan untuk melakukan insert data ke dalam database.

for _, album := range albums {
  _, err := tx.ExecContext(ctx, query, album.Title, album.Artist, album.Price)
  if err != nil {
    log.Printf("error execute insert err: %v", err)
    continue
  }
}

Saat melakukan create data ke dalam database kita menggunakan fungsi ExecContext yang mana berguna untuk menyimpan sementara di dalam database. Dan diakhiri dengan Commit() untuk menyimpan secara permanen ke dalam database.

err = tx.Commit()
if err != nil {
  return err
}

Melakukan Ubah Data Album

Melakukan perubahan dari satu data kita perlu membuat fungsi yang mengirimkan parameter id untuk menyeleksi data yang akan diubah. Berikut ini fungsi lengkapnya untuk mengubah album.

func (p *PostgresService) Update(album Album) error {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `UPDATE album set title=$1, artist=$2, price=$3 WHERE id=$4`
	result, err := p.db.ExecContext(ctx, query, album.Title, album.Artist, album.Price, album.ID)
	if err != nil {
		return err
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}

	fmt.Printf("Affected update : %d", rows)
	return nil
}

Perintah ExecContext digunakan sama halnya dengan tambah karena operasi ini bisa kita gunakan asalkan query-nya untuk kebutuhan update dalam hal ini kita gunakan untuk update data.

query := `UPDATE album set title=$1, artist=$2, price=$3 WHERE id=$4`
result, err := p.db.ExecContext(ctx, query, album.Title, album.Artist, album.Price, album.ID)
if err != nil {
  return err
}

Perintah selanjutnya yaitu memastikan apakah data tersebut berhasil berubah atau tidak. Disini kita menggunakan fungsi RowsAffected() yang memastikan apakah terdapat baris data yang sudah terupdate setelah operasi ini atau tidak.

rows, err := result.RowsAffected()
if err != nil {
  return err
}

Menghapus data Album

Operasi atau fungsi untuk menghapus data pada program ini kita menghapus satu persatu berdasarkan input parameter dari fungsi tersebut. Berikut fungsi lengkapnya.

func (p *PostgresService) Delete(id int64) error {
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	query := `DELETE from album WHERE id=$1`
	result, err := p.db.ExecContext(ctx, query, id)
	if err != nil {
		return err
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return err
	}
	fmt.Printf("Affected delete : %d", rows)
	return nil
}

Pada fungsi ini kita memanggil ExecContext untuk menghapus data ke dalam database. Hal ini digunakan sama halnya seperti update dan insert.

query := `DELETE from album WHERE id=$1`
result, err := p.db.ExecContext(ctx, query, id)
if err != nil {
  return err
}

Dilanjutkan juga dengan perintah RowsAffected() agar memastikan data yang akan kita delete itu sukses atau tidak ditemukan pada database.

rows, err := result.RowsAffected()
if err != nil {
  return err
}

Melakukan inisialisasi module Service pada Main

Setelah kita membuat program service yang mana kita membuat fungsi masing-masih untuk operasi ke dalam database saatnya kita mengintegrasikan semua tersebut ke dalam main program. Berikut ini main fungsi selengkapnya.

type app struct {
	AlbumService services.AlbumService
}

func main() {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("Error loading .env file")
	}

	db, err := openDB(os.Getenv("POSTGRES_URL"), true)
	if err != nil {
		log.Fatalln(err)
	}
	defer func(db *sql.DB) {
		err := db.Close()
		if err != nil {
			log.Fatalln(err)
		}
	}(db)

	application := app{AlbumService: services.NewPostgresService(db)}

	albums, err := application.AlbumService.GetAllAlbum()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("all album : %v\n", albums)

	albumNo1, err := application.AlbumService.Get(1)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("album number 1 : %v\n", albumNo1)

	err = application.AlbumService.BatchCreate([]services.Album{
		{Title: "Hari Yang Cerah", Artist: "Peterpan", Price: 50000},
		{Title: "Sebuah Nama Sebuah Cerita", Artist: "Peterpan", Price: 50000},
		{Title: "Bintang Di surga", Artist: "Peterpan", Price: 60000},
	})
	if err != nil {
		log.Fatal(err)
	}

	albumNo1.Price = 70000
	err = application.AlbumService.Update(*albumNo1)
	if err != nil {
		log.Fatal(err)
	}

	err = application.AlbumService.Delete(albumNo1.ID)
	if err != nil {
		log.Fatal(err)
	}
}

Mari kita bahas satu per satu perintah apa saja yang di jalankan pada fungsi main.

Mengambil konfigurasi dari file .env

Pada program main ini kita menyimpan semua konfigurasi koneksi ke dalam database disimpan pada file terpisah misalnya kita menyimpan di dalam file .env.

err := godotenv.Load(".env")
if err != nil {
  log.Fatalf("Error loading .env file")
}

Perintah diatas digunakan untuk meload data konfigurasi dalam file .env. lalu disimpan ke dalam env server atau komputer yang sedang dijalankan. Biasanya untuk mengambil data dari environment tersebut bisa dengan perintah seperti ini.

os.Getenv("<nama-konfigurasi>")

Dalam hal ini kita menyimpan koneksi URL ke dalam database yang disimpan ke dalam environment sehingga kita perlu mengambil data konfigurasinya itu menggunakan perintah os.Getenv("POSTGRES_URL").

Selanjutnya kita inisialisasi koneksi ke dalam database.

db, err := openDB(os.Getenv("POSTGRES_URL"), true)
if err != nil {
  log.Fatalln(err)
}

Nah saat ini kita perlu juga memastikan database koneksi ini perlu diakhiri close. Bisa juga kita gunakan menggunakan defer.

defer func(db *sql.DB) {
  err := db.Close()
  if err != nil {
    log.Fatalln(err)
  }
}(db)

Agar service kita melakukan akses pada main program maka kita perlu melakukan inisialisasi module service-nya.

application := app{AlbumService: services.NewPostgresService(db)}

Sisanya adalah pemanggilan fungsi-fungsi service yang sudah kita definisikan pada fungsi.

comments powered by Disqus