Kenapa saya berhenti menggunakan library ORM pada proyek Golang
ORM (Object Relational Mapping) merupakan sebuah library yang umumnya digunakan pada aplikasi back-end guna mempermudah proses query database serta schema migration. Developer hanya perlu mendefinisikan schema untuk setiap model, menjalankan migration, dan kemudian memanggil kode-kode yang telah disediakan oleh library ORM tersebut untuk menjalankan query pada database. Namun, terdapat abstraction yang berlebihan pada ORM (bahkan terkadang yang tidak diperlukan) yang dapat mengakibatkan penurunan performa saat menjalankan query data.
Terdapat pendekatan lain untuk menjalankan query database tanpa mengorbankan performa, yaitu dengan mengirimkan query SQL (Structured Query Language) langsung ke database. Namun, kelemahan dari metode ini adalah memerlukan penulisan kode yang lebih banyak dan hasil query yang tidak "type-safety".
Salah satu solusi yang saya temukan untuk kedua masalah ini adalah menggunakan "sql-to-code compiler" seperti sqlc. Berbeda dengan ORM pada umumnya, di sqlc kita perlu menuliskan terlebih dahulu query dan schema menggunakan Bahasa SQL. Selanjutnya, kode SQL tersebut akan di-compile menjadi kode Golang yang type-safety dan siap digunakan.
Pada umumnya, ORM menyediakan fitur untuk melakukan migration data (bahkan GORM dapat melakukan migration secara otomatis), namun sqlc tidak memiliki kemampuan ini secara out-of-the-box. Karena migration bukanlah fitur utama sqlc, kita perlu menggunakan tool lain untuk mempermudah proses migration.
Setelah mempertimbangkan berbagai pilihan migration tool, saya akhirnya memilih dbmate. Alasan di balik pilihan ini adalah dbmate menawarkan fitur yang paling lengkap dan command yang lebih sederhana dibandingkan dengan migration tool lainnya.
Berikut adalah perbandingan fitur yang dimiliki schema migration tool lain:
dbmate | goose | sql-migrate | golang-migrate | activerecord | sequelize | flyway | sqitch | |
Features | ||||||||
Plain SQL migration files | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ||
Support for creating and dropping databases | ✅ | ✅ | ||||||
Support for saving schema dump files | ✅ | ✅ | ||||||
Timestamp-versioned migration files | ✅ | ✅ | ✅ | ✅ | ✅ | |||
Custom schema migrations table | ✅ | ✅ | ✅ | ✅ | ||||
Ability to wait for database to become ready | ✅ | |||||||
Database connection string loaded from environment variables | ✅ | ✅ | ||||||
Automatically load .env file | ✅ | |||||||
No separate configuration file | ✅ | ✅ | ✅ | ✅ | ✅ | |||
Language/framework independent | ✅ | ✅ | ✅ | ✅ | ✅ | |||
Drivers | ||||||||
PostgreSQL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
MySQL | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
SQLite | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
CliсkHouse | ✅ | ✅ | ✅ | ✅ |
Sumber tabel: https://github.com/amacneil/dbmate#alternatives
Sebelumnya, saya telah menggunakan goose. Namun, saya harus mengetikkan command yang cukup panjang untuk menggunakan tool ini karena harus selalu menyertakan "database url" sebagai argumen untuk tool tersebut. Berbeda halnya dengan dbmate, yang dapat membaca database url langsung dari file .env
.
# bayangkan kalian harus mengetikan command sepanjang ini setiap menggunakan goose
goose postgres "user=postgres password=postgres dbname=postgres sslmode=disable" up
Memang, proses setup sqlc membutuhkan langkah yang lebih rumit dibandingkan dengan penggunaan ORM pada umumnya. Dengan sqlc, kita perlu menulis file konfigurasi sqlc dan kode SQL untuk membuat tabel dan melakukan query CRUD, sehingga membutuhkan pemahaman yang baik tentang SQL.
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "./internal/infrastructure/database/postgres/query"
schema: "./internal/infrastructure/database/postgres/migration"
gen:
go:
sql_package: "pgx/v5"
out: "./internal/infrastructure/database/postgres/sqlc"
emit_empty_slices: true
omit_unused_structs: true
emit_interface: true
Meskipun begitu, penggunaan sqlc memiliki beberapa keunggulan, di antaranya:
Performa yang lebih cepat karena tidak tergantung pada third-party library (namun masih memerlukan database driver, yang merupakan hal yang wajar).
Hasil fetch menggunakan ORM (4.13 ms):
Hasil fetch menggunakan sqlc (1.44 ms) 🚀:
Kemampuan untuk memanfaatkan fitur bawaan dari DBMS secara maksimal. Sebagai contoh, di PostgreSQL, kita dapat memanfaatkan fitur enum.
-- 20230819005847_create_user_table.sql -- berikut adalah kode untuk schema/migration yang diperlukan oleh sqlc dan dbmate -- migrate:up CREATE TYPE role AS ENUM ('user', 'admin'); CREATE TABLE "user" ( id SERIAL PRIMARY KEY NOT NULL, full_name VARCHAR(50) NOT NULL, username VARCHAR(16) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, password TEXT NOT NULL, role role DEFAULT 'user', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- migrate:down DROP TABLE "user"; DROP TYPE role;
Tidak ada abstraction yang tersembunyi, karena kode yang dihasilkan sepenuhnya didasarkan pada kode SQL yang telah dituliskan sebelumnya.
-- user_query.sql -- name: CreateUser :one INSERT INTO "user" ( full_name, username, email, password, role ) VALUES ( $1, $2, $3, $4, $5 ) RETURNING id, full_name, username, email, role, created_at; -- name: FindAllUsers :many SELECT id, full_name, username, email, role, created_at, updated_at FROM "user"; -- name: FindOneUserByID :one SELECT * FROM "user" WHERE id = $1 LIMIT 1; -- name: FindOneUserByEmail :one SELECT * FROM "user" WHERE email = $1 LIMIT 1; -- name: FindAdmin :many SELECT * FROM "user" WHERE role = 'admin'::role; -- name: UpdateUser :one UPDATE "user" SET full_name = $2, username = $3, updated_at = $4 WHERE id = $1 RETURNING id, full_name, username, email, role, created_at, updated_at; -- name: UpdateEmail :one UPDATE "user" SET email = $2, updated_at = $3 WHERE id = $1 RETURNING id, full_name, username, email, role, created_at, updated_at; -- name: UpdatePassword :exec UPDATE "user" SET password = $2, updated_at = $3 WHERE id = $1; -- name: DeleteUser :exec DELETE FROM "user" WHERE id = $1;
Kode Golang hasil compile sqlc:
// models.go // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.19.1 package sqlc import ( "database/sql/driver" "fmt" "github.com/jackc/pgx/v5/pgtype" ) type Role string const ( RoleUser Role = "user" RoleAdmin Role = "admin" ) func (e *Role) Scan(src interface{}) error { switch s := src.(type) { case []byte: *e = Role(s) case string: *e = Role(s) default: return fmt.Errorf("unsupported scan type for Role: %T", src) } return nil } type NullRole struct { Role Role Valid bool // Valid is true if Role is not NULL } // Scan implements the Scanner interface. func (ns *NullRole) Scan(value interface{}) error { if value == nil { ns.Role, ns.Valid = "", false return nil } ns.Valid = true return ns.Role.Scan(value) } // Value implements the driver Valuer interface. func (ns NullRole) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } return string(ns.Role), nil } type ChangeEmailToken struct { Token string NewEmail string ExpiresAt pgtype.Timestamp UserID pgtype.Int4 } type RefreshToken struct { Token string UserID pgtype.Int4 } type ResetPasswordToken struct { Token string ExpiresAt pgtype.Timestamp UserID pgtype.Int4 } type User struct { ID int32 FullName string Username string Email string Password string Role NullRole CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp }
... const createUser = `-- name: CreateUser :one INSERT INTO "user" ( full_name, username, email, password, role ) VALUES ( $1, $2, $3, $4, $5 ) RETURNING id, full_name, username, email, role, created_at ` type CreateUserParams struct { FullName string Username string Email string Password string Role NullRole } type CreateUserRow struct { ID int32 FullName string Username string Email string Role NullRole CreatedAt pgtype.Timestamp } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) { row := q.db.QueryRow(ctx, createUser, arg.FullName, arg.Username, arg.Email, arg.Password, arg.Role, ) var i CreateUserRow err := row.Scan( &i.ID, &i.FullName, &i.Username, &i.Email, &i.Role, &i.CreatedAt, ) return i, err } ...
Pada post berikutnya, saya akan menjelaskan langkah-langkah menggunakan sqlc dan dbmate dalam proyek golang sederhana. Jika kalian ingin melihat bagaimana saya menggunakan sqlc dan dbmate pada proyek saya yang sudah meng-implementasikan clean architecture, kalian bisa cek repository saya https://codeberg.org/tfkhdyt/blog-api.
Jangan lupa untuk subscribe ke blog ini agar kalian tidak melewatkan setiap post baru yang saya buat.
Akhir kata saya ucapkan terima kasih telah membaca post ini. Semoga post ini dapat membangkitkan minat kalian untuk mencoba sqlc dan dbmate dalam proyek back-end kalian.
Subscribe to my newsletter
Read articles from Taufik Hidayat directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Taufik Hidayat
Taufik Hidayat
Hello, world! My name is Taufik Hidayat. I'm a Junior Back-end Developer, Computer Science Student, YouTuber, Free and Open Source Software Enthusiast, and GNU/Linux Nerd. I live in Bandung, Indonesia. I was born in Majalengka on April 1, 2002 (21 years old). I have experience as a Junior Web Developer. I have an interest in a career as a Back-end Developer (TypeScript and Golang). I’m a fast learner and self-taught. I have learned a lot of new technologies in the past few years by myself on the Internet.