Di sini kita akan membahas bagaimana menggunakan sqlc
dalam sebuah proyek aplikasi back-end bahasa Go dengan database MySQL.
Daftar Isi
- Daftar Isi
- Faktor Kebutuhan akan
sqlc
- Alur Singkat Penggunaan
sqlc
- Ayo Praktik!
Faktor Kebutuhan akan sqlc
Berbeda dari ORM seperti GORM, sqlc
tidak menyembunyikan SQL-nya. Justru sebaliknya, kita menulis query SQL sendiri, dan sqlc
akan me-generate function-function Go yang sudah aman dari kesalahan tipe dan siap dipanggil.
Kesalahan tipe rentan terjadi baik saat melakukan binding row dari
SELECT
maupun mengisi parameter untukINSERT
,UPDATE
, danDELETE
.
Alur Singkat Penggunaan sqlc
- Tulis query dalam file .sql
- Jalankan command
sqlc generate
- Function Go tersedia dalam package yang kita tentukan dan siap digunakan! 🔥
Ayo Praktik!
Dokumentasi resmi dapat diakses di sini ya.
(Juga terdapat untuk database PostgreSQL dan SQLite)
A. Instalasi dan Setup
Struktur Direktori Proyek
Secara garis besar, kita akan membentuk folder proyek kita sebagai berikut.
learn-sqlc/
├── cmd/
│ └── main.go
├── db/
│ ├── migrations/
│ ├── queries/
│ └── sqlc/
└── repository/
Instal sqlc
Jalankan command berikut di terminal!
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
Oiya, di sini saya mencontohkan schema dalam file-file yang di-generate oleh golang-migrate
. Jika mau ikut menggunakannya juga bisa memulai dari sini.
Setup sqlc
pada folder proyek
Pada folder root, buat file bernama sqlc.yaml
dengan isi seperti berikut.
version: "2"
sql:
- engine: "mysql"
queries: "db/queries"
schema: "db/migrations"
gen:
go:
package: "sqlc"
out: "db/sqlc"
sql_package: "database/sql"
sql_driver: "github.com/go-sql-driver/mysql"
Semisal tidak menggunakan
golang-migrate
, bisa buat saja nama foldernya sepertidb/schema
dengan isi sebagai berikut.
db/
├── schema/
│ ├── file1.sql
│ ├── file2.sql
│ └── dst.
Lalu nanti ambil isi file xx.up.sql saja pada contoh-contoh yang saya cantumkan (tidak usah yang "down")
Catatan Penting:
- value dari key
package:
akan digunakan sebagai nama package di awal setiap file .go yang di-generate olehsqlc
- value dari key
out:
akan menjadi folder target tempat meletakkan file-file .go yang di-generate olehsqlc
- pastikan menambahkan dependency modul driver SQL tersebut, bisa gunakan command berikut
go get -u github.com/go-sql-driver/mysql
, lalu import di file yang pasti tersentuh saat menjalankan runtime 😉
B. Membuat Queries dan Generate Function Go
1. Membuat schema pada database
Berikut adalah ERD yang akan kita implementasikan.
Saya membuatnya di eraser.io/cody
Saya telah membuat file migrasi di dalam folder db/migrations
dengan isi seperti berikut.
.up.sql
CREATE TABLE IF NOT EXISTS teams (
id CHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(50) NULL
) ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS participants (
id BIGINT AUTO_INCREMENT NOT NULL PRIMARY KEY,
team_id CHAR(36) NOT NULL,
name VARCHAR(50) NULL,
CONSTRAINT fk_participant_team FOREIGN KEY (team_id) REFERENCES teams(id)
) ENGINE = InnoDB;
.down.sql
DROP TABLE IF EXISTS participants;
DROP TABLE IF EXISTS teams;
2. Membuat queries dan mencoba generate
Perlu diketahui 🔈
Pola membuat kueri untuk sqlc
adalah:
--name :{Nama Function}: {jenis permintaan}
{kueri}
team.sql
-- name: CreateTeam :execresult
INSERT INTO teams (
id, name
) VALUES (?, ?);
Sekarang kita akan mencoba melakukan generate function Go. Jalankan command berikut di terminal!
sqlc generate
Jika kita memeriksa folder db/sqlc
saat ini, kita akan menemukan file-file dengan format .go .
a. File db.go memuat isi seperti struct Queries
dan constructornya. Queries
(dengan properti db
nya) ini lah yang akan berlaku sebagai 'database handle' yang melakukan semacam (DB).Exec
dan (DB).Query
.
b. File model.go berisi struct-struct yang mewakili row berproperti persis setiap tabel yang ada berdasarkan sumber schema: dalam proyek ini adalah file-file dalam folder migrations
.
Jika kita melakukan
SELECT * FROM . . .
, function hasil generate-nya akan di-bind langsung ke struct dari model.go
c. File-file berjumlah sesuai dengan jumlah file dalam folder queries
, misalnya team.sql.go , berisi function-function yang di-generate berdasarkan sintaks SQL yang ada di dalam team.sql .
Di sini lah sqlc
mendefinisikan function-function Go yang akan kita gunakan nantinya!
Perhatikan di dalam team.sql.go !
team.sql.go
// inisialisasi string createTeam sesuai sintaks SQL kita
const createTeam = `...`
type CreateTeamParams struct {
ID string
Name sql.NullString
}
func (q *Queries) CreateTeam(ctx context.Context, arg CreateTeamParams) (sql.Result, error) {
return q.db.ExecContext(ctx, createTeam, arg.ID, arg.Name)
}
Kita sudah berhasil membuat satu buah function Go dengan nama CreateTeam()
😮
Perhatikan bahwa function CreateTeam()
mengembalikan sql.Result
dan error
. sql.Result
umumnya digunakan untuk mengambil last insert ID ataupun jumlah rows affected saat saat melakukan kueri yang berkarakteristik 'mutation': INSERT
, UPDATE
, DELETE
.
Coba ubah keyword execresult
menjadi exec
saja lalu jalankan sqlc generate
! Periksa bagaimana perbedaan function Go yang terbentuk!
Selain execresult
dan exec
, juga ada jenis permintaan ini:
-
execrows
: langsung mengembalikan jumlah affected rows:(int64, error)
-
execlastid
: langsung mengembalikan ID baru yang terbentuk:(int64, error)
Jika ingin langsung mencoba penggunaan function CreateTeam()
ini, bisa langsung lompat ke bagian ini dulu ya!
3. Generate queries - naming parameters
Mari tambahkan function lagi pada file team.sql.go !
team.sql
-- name: CreateTeam :execresult
INSERT INTO teams (
id, name
) VALUES (?, ?);
-- name: GetAllTeams :many
SELECT * FROM teams;
-- name: GetTeamIDByName :one
SELECT id FROM teams
WHERE name = ?;
jangan lupa di-save 😄
Lalu jalankan command sqlc generate
!
Sekarang kita menemukan dua function baru di dalam team.sql.go .
sqlc
sebenarnya tetap melakukan generate seluruhnya (termasukCreateTeam()
)
team.sql.go
func (q *Queries) GetAllTeams(ctx context.Context) ([]Team, error) {
rows, err := q.db.QueryContext(ctx, getAllTeams)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Team
for rows.Next() {
var i Team
if err := rows.Scan(&i.ID, &i.Name); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
func (q *Queries) GetTeamIDByName(ctx context.Context, name sql.NullString) (string, error) {
row := q.db.QueryRowContext(ctx, getTeamIDByName, name)
var id string
err := row.Scan(&id)
return id, err
}
Mengubah nama parameter
Perhatikan bahwa function GetTeamIDByName()
meminta argumen bertipe sql.NullString
dengan sebutan name
. Secara default, sqlc
menggunakan nama kolom dari tabel sebagai nama parameter. Mungkin saja kita merasa lebih nyaman jika nama parameter tersebut lebih spesifik, misalnya teamName
. Kita dapat mengkustomisasi nama parameter sebagai berikut.
Ada dua macam "naming parameter" pada sqlc
, yaitu sqlc.arg()
dan juga sqlc.narg()
(untuk parameter yang nullable).
Mari memodifikasi kode file team.sql !
team.sql
-- name: GetTeamIDByName :one
SELECT id FROM teams
WHERE name = sqlc.arg('team_name');
Jalankan sqlc generate
!
Dan coba periksa kode function GetTeamIDByName()
saat ini!
team.sql.go
func (q *Queries) GetTeamIDByName(ctx context.Context, teamName sql.NullString) (string, error) {
row := q.db.QueryRowContext(ctx, getTeamIDByName, teamName)
var id string
err := row.Scan(&id)
return id, err
}
Nama parameter telah berubah menjadi teamName
! 😉
Informasi tambahan:
- Saat ada lebih dari satu parameter dalam kueri tertentu, function Go dari
sqlc
akan meminta parameter kedua (parameter setelah context) berupa satu buah struct yang membalut semua parameter yang diperlukan, contohnya seperti structCreateTeamParams
yang berisi propertiID
danName
. - Sedangkan jika hanya ada satu buah parameter dalam kueri SQL tertentu, function Go dari
sqlc
akan meminta parameter tersebut sebagai parameter kedua pada function, contohnya sepertiname
atauteamName
padaGetTeamIDByName()
. - Begitu juga dengan row! Saat kueri hanya me-
SELECT
satu buah kolom makasqlc
akan me-return row tanpa membuat struct. Contohnya yaitu seperti kembalian string padaGetTeamIDByName()
.
4. Generate queries - specified rows
Perhatikan pada contoh sebelumnya return dari function GetAllTeams()
adalah objek bertipe struct Team
.
func (q *Queries) GetAllTeams(ctx context.Context) ([]Team, error) {
... ^^^^
}
Hal ini akan selalu terjadi ketika kita mengambil * FROM {table}
, rows akan di-bind ke struct yang terdefinisi dari file model.go .
Kita sebenarnya sudah mencoba membuat kueri dengan mengambil kolom id
saja. Namun, apa yang terjadi jika yang kita minta bukan *
dan lebih dari satu kolom?
Sekarang, coba buat file participant.sql di dalam folder db/queries
dengan isi sebagai berikut!
participant.sql
-- name: GetParticipantByName :one
SELECT
p.*, -- atau p.id, p.name, p.team_id
t.name
FROM participants p
LEFT JOIN teams t ON p.team_id = t.id
WHERE p.name = sqlc.arg(participant_name);
Lalu jalankan command sqlc generate
!
Saat ini, di dalam folder db/sqlc
telah terbentuk sebuah file baru bernama participant.sql.go . Di dalamnya, sebelum definisi dari function GetParticipantByName()
, kita akan menemukan definisi dari struct berikut.
type GetParticipantByNameRow struct {
ID int64
TeamID string
Name sql.NullString
Name_2 sql.NullString
}
Struct ini lah yang digunakan oleh sqlc
untuk mem-bind row yang didapatkan dari MySQL untuk kemudian menjadi return dari functionnya. Secara default, sqlc
akan mendefinisikan struct tersebut dengan nama berformat {NamaFunction}Row
, dengan nama kolom menyesuaikan nama-nama kolom bentukan MySQL dari kueri tersebut.
Menggunakan AS pada nama kolom
Modifikasi isi file participant.sql menjadi seperti berikut!
participant.sql
-- name: GetParticipantByName :one
SELECT
p.id AS participant_id,
p.name AS participant_name,
p.team_id,
t.name AS team_name
FROM participants p
LEFT JOIN teams t ON p.team_id = t.id
WHERE p.name = sqlc.arg(participant_name);
Lalu generate kembali!
Inilah struct yang akan terdefinisikan dalam participant.sql.go .
type GetParticipantByNameRow struct {
ParticipantID int64
ParticipantName sql.NullString
TeamID string
TeamName sql.NullString
}
sqlc
akan mengikuti kolom yang terbentuk dari kueri SQL kita, baik pada nama kolom maupun urutan kolom.
C. Menggunakan Function Hasil Generate
1. Menggunakan function DML
Apa itu DML dan DQL? Sudah dibahas cukup lengkap oleh @ronnychen dalam artikel ini
Kita akan menggunakan function CreateTeam()
yang ada pada team.sql.go .
Pada folder repository
, buat file team_repository.go !
team_repository.go
type TeamRepository interface {
CreateTeam(name string)
}
type teamRepository struct {
db *sqlc.Queries
}
// constructor
func NewTeamRepository(q *sqlc.Queries) TeamRepository {
return &teamRepository{db: q}
}
func (r *teamRepository) CreateTeam(name string) {
params := sqlc.CreateTeamParams{
ID: uuid.NewString()
Name: sql.NullString{String: name, Valid: true}
}
err := r.db.CreateTeam(context.Background(), params)
if err != nil {
fmt.Println("Failed to insert new team to database")
return
}
}
Lalu kita panggil pada cmd/main.go .
main.go
func main() {
godotenv.Load()
db := config.InitDB()
defer db.Close()
queries := sqlc.New(db)
teamRepo := repository.NewTeamRepository(queries)
teamRepo.CreateTeam("Team A")
teamRepo.CreateTeam("Team B")
}
Sekarang cek pada database! Lihat isi tabel teams
untuk memastikan data team baru sudah benar-benar ter-insert 😎
Menggunakan sql.Result
untuk last insert ID
Masih ingat bahwa id
pada schema tabel participants
tadi kita buat dengan atribut AUTO_INCREMENT
?? Yap, id
-nya tidak kita masukkan secara manual, tetapi diset oleh MySQL. Jika kita membutuhkan id
baru tersebut, kita dapat mengambilnya dari sql.Result
.
Untuk mendapatkan return
sql.Result
jangan lupa menggunakanexecresult
(atau langsungexeclastid
) ketimbangexec
participant.sql
-- name: CreateParticipant :execresult
INSERT INTO participants (
name, team_id
) VALUES (?, ?);
sqlc generate
akan menghasilkan function berikut.
participant.sql.go
func (q *Queries) CreateParticipant(ctx context.Context, arg CreateParticipantParams) (sql.Result, error) {
...
}
function
CreateParticipant()
mengembalikansql.Result
danerror
Buat file participant_repository.go !
participant_repository.go
type ParticipantRepository interface {
CreateParticipant(name, teamID string)
}
type participantRepository struct {
db *sqlc.Queries
}
// constructor
func NewParticipantRepository(q *sqlc.Queries) ParticipantRepository {
return &participantRepository{db: q}
}
func (r *participantRepository) CreateParticipant(name, teamID string) {
params := sqlc.CreateParticipantParams {
Name: sql.NullString{String: name, Valid: true},
TeamID: teamID,
}
result, err := r.db.CreateParticipant(context.Background(), params)
if err != nil {
fmt.Printf("Failed to insert new participant to database")
return
}
ID, _ := result.LastInsertId()
fmt.Printf("Successfully inserted new participant with ID: %d\n", ID)
}
Kita akan mencoba memanggil CreateParticipant()
ini di main.go . Parameter yang dibutuhkan untuk menambahkan data partisipan ke database adalah nama partisipan dan id grup. Kita akan menyediakan function GetTeamIDByName()
terlebih dahulu 😉
team_repository.go
func (r *teamRepository) GetTeamIDByName(name string) string {
row, err := r.db.GetTeamIDByName(context.Background(), sql.NullString{
String: name,
Valid: true,
})
if err != nil {
panic(errors.New("failed to get team ID"))
}
return row
}
Oke, sekarang kita tinggal melengkapi main.go !
Agar method/function bisa diakses, jangan lupa mencantumkan function yang telah ditambahkan ke interface
{X}Repository
ya!
main.go
participantRepo := repository.NewParticipantRepository(queries)
participantRepo.CreateParticipant("John1", teamRepo.GetTeamIDByName("Team B"))
participantRepo.CreateParticipant("John2", teamRepo.GetTeamIDByName("Team A"))
participantRepo.CreateParticipant("John3", teamRepo.GetTeamIDByName("Team B"))
Periksa di database bahwasanya data partisipan telah ditambahkan!
Menggunakan sql.Result
untuk rows affected
Selain mengambil ID terakhir, sql.Result
juga dapat memuat jumlah baris yang terpengaruh oleh eksekusi kueri, misalnya pada kueri UPDATE
dan DELETE
.
team.sql
-- name: UpdateTeamName :execresult
UPDATE teams SET name = sqlc.arg('new_name')
WHERE name LIKE sqlc.arg('old_name');
Generate, lalu buat function untuk TeamRepository
!
team_repository.go
func (r *teamRepository) ChangeTeamName(old, new string) {
params := sqlc.UpdateTeamParams {
OldName: sql.NullString{String: old, Valid: true},
NewName: sql.NullString{String: new, Valid: true},
}
result, err := r.db.UpdateTeamName(context.Background(), params)
if err != nil {
fmt.Printf("There were some issues when updating the team: %v\n", err)
return
}
affected, _ := result.RowsAffected()
fmt.Printf("Successfully updated the name of %d team(s)", affected)
}
Coba di main.go dengan argumen sebagai berikut.
main.go
teamRepo.UpdateTeamName("Team %", "Unused Team from Prev. Chapter")
Lalu perhatikan log yang tercetak di konsol untuk memeriksa nilai affected
yang kita dapatkan!
2. Menggunakan function DQL
Kita akan menggunakan function GetAllTeams()
untuk kemudian mencetak data tim pada konsol.
Lengkapi file team_repository.go !
team_repository.go
func printTeam(row sqlc.Team, num int) {
fmt.Printf("\nTeam #%d\n", num)
fmt.Printf("ID\t: %s\n", row.ID)
fmt.Printf("Name\t: %s\n", row.Name.String)
}
func (r *teamRepository) GetAllTeams() {
rows, err := r.db.GetAllTeams(context.Background())
if err != nil {
fmt.Printf("Failed to fetch teams from database: %v", err)
return
}
for i, row := range rows {
printTeam(row, i)
}
}
Kita mengambil id tim dengan row.ID
dan nama tim dengan row.Name.String
(yaitu value properti String
pada sql.NullString
).
Coba panggil di main.go !
Percobaan terakhir
Untuk yang terakhir, kita akan mencetak data partisipan beserta timnya.
Lengkapi participant.sql !
participant.sql
-- name: GetParticipantByName :one
SELECT
p.id AS participant_id,
p.name AS participant_name,
p.team_id,
t.name AS team_name
FROM participants p
LEFT JOIN teams t ON p.team_id = t.id
WHERE p.name = sqlc.arg(participant_name);
Generate, lalu lengkapi participant_repository.go !
Perhatikan bahwasanya kembalian dari function yang dibuat oleh sqlc
bertipe struct GetParticipantByNameRow
dengan properti:
-
ParticipantID
, bertipe int64 -
ParticipantName
, bertipesql.NullString
-
TeamID
, bertipe string -
TeamName
, bertipesql.NullString
participant_repository.go
func printParticipant(row sqlc.GetParticipantByNameRow) {
fmt.Println("\nPrinting participant data . . .")
fmt.Printf("ID\t\t: %d\n", row.ParticipantID)
fmt.Printf("Name\t\t: %s\n", row.ParticipantName.String)
fmt.Printf("Team ID\t\t: %s\n", row.TeamID)
fmt.Printf("Team Name\t: %s\n\n", row.TeamName.String)
}
func (r *participantRepository) GetParticipantByName(name string) {
row, err := r.db.GetParticipantByName(context.Background(), sql.NullString{
String: name,
Valid: true,
})
if err != nil {
fmt.Println("Failed to fetch participant from database")
return
}
printParticipant(row)
}
Sekarang kita panggil di main.go .
main.go
participantRepo.GetParticipantByName("John3")
Begini contoh hasil yang didapatkan.
Top comments (0)