在 Go 語言中編寫數據庫操做代碼真的很是痛苦!database/sql
標準庫提供的都是比較底層的接口。咱們須要編寫大量重複的代碼。大量的模板代碼不只寫起來煩,並且還容易出錯。有時候字段類型修改了一下,可能就須要改動不少地方;添加了一個新字段,以前使用select *
查詢語句的地方都要修改。若是有些地方有遺漏,可能就會形成運行時panic
。即便使用 ORM 庫,這些問題也不能徹底解決!這時候,sqlc
來了!sqlc
能夠根據咱們編寫的 SQL 語句生成類型安全的、地道的 Go 接口代碼,咱們要作的只是調用這些方法。mysql
先安裝:linux
$ go get github.com/kyleconroy/sqlc/cmd/sqlc
複製代碼
固然還有對應的數據庫驅動:git
$ go get github.com/lib/pq
$ go get github.com/go-sql-driver/mysql
複製代碼
sqlc
是一個命令行工具,上面代碼會將可執行程序sqlc
放到$GOPATH/bin
目錄下。我習慣把$GOPATH/bin
目錄加入到系統PATH
中。因此能夠執行使用這個命令。github
由於sqlc
用到了一個 linux 下的庫,在 windows 上沒法正常編譯。在 windows 上咱們可使用 docker 鏡像kjconroy/sqlc
。docker 的安裝就不介紹了,網上有不少教程。拉取kjconroy/sqlc
鏡像:golang
$ docker pull kjconroy/sqlc
複製代碼
而後,編寫 SQL 語句。在schema.sql
文件中編寫建表語句:sql
CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT
);
複製代碼
在query.sql
文件中編寫查詢語句:docker
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :exec
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING *;
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;
複製代碼
sqlc
支持 PostgreSQL 和 MySQL,不過對 MySQL 的支持是實驗性的。期待後續完善對 MySQL 的支持,增長對其它數據庫的支持。本文咱們使用的是 PostgreSQL。編寫數據庫程序時,上面兩個 sql 文件是少不了的。sqlc
額外只須要一個小小的配置文件sqlc.yaml
:數據庫
version: "1"
packages:
- name: "db"
path: "./db"
queries: "./query.sql"
schema: "./schema.sql"
複製代碼
version
:版本;packages
:
name
:生成的包名;path
:生成文件的路徑;queries
:查詢 SQL 文件;schema
:建表 SQL 文件。在 windows 上執行下面的命令生成對應的 Go 代碼:json
docker run --rm -v CONFIG_PATH:/src -w /src kjconroy/sqlc generate
複製代碼
上面的CONFIG_PATH
替換成配置所在目錄,個人是D:\code\golang\src\github.com\darjun\go-daily-lib\sqlc\get-started
。sqlc
爲咱們在同級目錄下生成了數據庫操做代碼,目錄結構以下:windows
db
├── db.go
├── models.go
└── query.sql.go
複製代碼
sqlc
根據咱們schema.sql
和query.sql
生成了模型對象結構:
// models.go
type Author struct {
ID int64
Name string
Bio sql.NullString
}
複製代碼
和操做接口:
// query.sql.go
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) 複製代碼
其中Queries
是sqlc
封裝的一個結構。
說了這麼多,來看看如何使用:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
"golang.org/x/net/context"
"github.com/darjun/go-daily-lib/sqlc/get-started/db"
)
func main() {
pq, err := sql.Open("postgres", "dbname=sqlc sslmode=disable")
if err != nil {
log.Fatal(err)
}
queries := db.New(pq)
authors, err := queries.ListAuthors(context.Background())
if err != nil {
log.Fatal("ListAuthors error:", err)
}
fmt.Println(authors)
insertedAuthor, err := queries.CreateAuthor(context.Background(), db.CreateAuthorParams{
Name: "Brian Kernighan",
Bio: sql.NullString{String: "Co-author of The C Programming Language and The Go Programming Language", Valid: true},
})
if err != nil {
log.Fatal("CreateAuthor error:", err)
}
fmt.Println(insertedAuthor)
fetchedAuthor, err := queries.GetAuthor(context.Background(), insertedAuthor.ID)
if err != nil {
log.Fatal("GetAuthor error:", err)
}
fmt.Println(fetchedAuthor)
err = queries.DeleteAuthor(context.Background(), insertedAuthor.ID)
if err != nil {
log.Fatal("DeleteAuthor error:", err)
}
}
複製代碼
生成的代碼在包db
下(由packages.name
選項指定),首先調用db.New()
將sql.Open()
的返回值sql.DB
做爲參數傳入,獲得Queries
對象。咱們對authors
表的操做都須要經過該對象的方法。
上面程序要運行,還須要啓動 PostgreSQL,建立數據庫和表:
$ createdb sqlc
$ psql -f schema.sql -d sqlc
複製代碼
上面第一條命令建立一個名爲sqlc
的數據庫,第二條命令在數據庫sqlc
中執行schema.sql
文件中的語句,即建立表。
最後運行程序(多文件程序不能用go run main.go
):
$ go run .
[]
{1 Brian Kernighan {Co-author of The C Programming Language and The Go Programming Language true}}
{1 Brian Kernighan {Co-author of The C Programming Language and The Go Programming Language true}}
複製代碼
除了 SQL 語句自己,sqlc
須要咱們在編寫 SQL 語句的時候經過註釋的方式爲生成的程序提供一些基本信息。語法爲:
-- name: <name> <cmd>
複製代碼
name
爲生成的方法名,如上面的CreateAuthor/ListAuthors/GetAuthor/DeleteAuthor
等,cmd
能夠有如下取值:
:one
:表示 SQL 語句返回一個對象,生成的方法的返回值爲(對象類型, error)
,對象類型能夠從表名得出;:many
:表示 SQL 語句會返回多個對象,生成的方法的返回值爲([]對象類型, error)
;:exec
:表示 SQL 語句不返回對象,只返回一個error
;:execrows
:表示 SQL 語句須要返回受影響的行數。:one
-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = $1 LIMIT 1
複製代碼
註釋中--name
指示生成方法GetAuthor
,從表名得出返回的基礎類型爲Author
。:one
又表示只返回一個對象。故最終的返回值爲(Author, error)
:
// db/query.sql.go
const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 `
func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
row := q.db.QueryRowContext(ctx, getAuthor, id)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
複製代碼
:many
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
複製代碼
註釋中--name
指示生成方法ListAuthors
,從表名authors
獲得返回的基礎類型爲Author
。:many
表示返回一個對象的切片。故最終的返回值爲([]Author, error)
:
// db/query.sql.go
const listAuthors = `-- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name `
func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
rows, err := q.db.QueryContext(ctx, listAuthors)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Author
for rows.Next() {
var i Author
if err := rows.Scan(&i.ID, &i.Name, &i.Bio); 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
}
複製代碼
這裏注意一個細節,即便咱們使用了select *
,生成的代碼中 SQL 語句被也改寫成了具體的字段:
SELECT id, name, bio FROM authors
ORDER BY name
複製代碼
這樣後續若是咱們須要添加或刪除字段,只要執行了sqlc
命令,這個 SQL 語句和ListAuthors()
方法就能保持一致!是否是很方便?
:exec
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1
複製代碼
註釋中--name
指示生成方法DeleteAuthor
,從表名authors
獲得返回的基礎類型爲Author
。:exec
表示不返回對象。故最終的返回值爲error
:
// db/query.sql.go
const deleteAuthor = `-- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1 `
func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteAuthor, id)
return err
}
複製代碼
:execrows
-- name: DeleteAuthorN :execrows
DELETE FROM authors
WHERE id = $1
複製代碼
註釋中--name
指示生成方法DeleteAuthorN
,從表名authors
獲得返回的基礎類型爲Author
。:exec
表示返回受影響的行數(即刪除了多少行)。故最終的返回值爲(int64, error)
:
// db/query.sql.go
const deleteAuthorN = `-- name: DeleteAuthorN :execrows DELETE FROM authors WHERE id = $1 `
func (q *Queries) DeleteAuthorN(ctx context.Context, id int64) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteAuthorN, id)
if err != nil {
return 0, err
}
return result.RowsAffected()
}
複製代碼
無論編寫的 SQL 多複雜,老是逃不過上面的規則。咱們只須要在編寫 SQL 語句時額外添加一行註釋,sqlc
就能爲咱們生成地道的 SQL 操做方法。生成的代碼與咱們本身手寫的沒什麼不一樣,錯誤處理都很完善,並且了避免手寫的麻煩與錯誤。
sqlc
爲全部的建表語句生成對應的模型結構。結構名爲表名的單數形式,且首字母大寫。例如:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name text NOT NULL
);
複製代碼
生成對應的結構:
type Author struct {
ID int
Name string
}
複製代碼
並且sqlc
能夠解析ALTER TABLE
語句,它會根據最終的表結構來生成模型對象的結構。例如:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
birth_year int NOT NULL
);
ALTER TABLE authors ADD COLUMN bio text NOT NULL;
ALTER TABLE authors DROP COLUMN birth_year;
ALTER TABLE authors RENAME TO writers;
複製代碼
上面的 SQL 語句中,建表時有兩列id
和birth_year
。第一條ALTER TABLE
語句添加了一列bio
,第二條刪除了birth_year
列,第三條將表名authors
改成writers
。sqlc
依據最終的表名writers
和表中的列id
、bio
生成代碼:
package db
type Writer struct {
ID int
Bio string
}
複製代碼
sqlc.yaml
文件中還能夠設置其餘的配置字段。
emit_json_tags
默認爲false
,設置該字段爲true
能夠爲生成的模型對象結構添加 JSON 標籤。例如:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
created_at timestamp NOT NULL
);
複製代碼
生成:
package db
import (
"time"
)
type Author struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
複製代碼
emit_prepared_queries
默認爲false
,設置該字段爲true
,會爲 SQL 生成對應的prepared statement
。例如,在快速開始的示例中設置這個選項,最終生成的結構Queries
中會添加全部 SQL 對應的prepared statement
對象:
type Queries struct {
db DBTX
tx *sql.Tx
createAuthorStmt *sql.Stmt
deleteAuthorStmt *sql.Stmt
getAuthorStmt *sql.Stmt
listAuthorsStmt *sql.Stmt
}
複製代碼
和一個Prepare()
方法:
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db}
var err error
if q.createAuthorStmt, err = db.PrepareContext(ctx, createAuthor); err != nil {
return nil, fmt.Errorf("error preparing query CreateAuthor: %w", err)
}
if q.deleteAuthorStmt, err = db.PrepareContext(ctx, deleteAuthor); err != nil {
return nil, fmt.Errorf("error preparing query DeleteAuthor: %w", err)
}
if q.getAuthorStmt, err = db.PrepareContext(ctx, getAuthor); err != nil {
return nil, fmt.Errorf("error preparing query GetAuthor: %w", err)
}
if q.listAuthorsStmt, err = db.PrepareContext(ctx, listAuthors); err != nil {
return nil, fmt.Errorf("error preparing query ListAuthors: %w", err)
}
return &q, nil
}
複製代碼
生成的其它方法都使用了這些對象,而非直接使用 SQL 語句:
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
row := q.queryRow(ctx, q.createAuthorStmt, createAuthor, arg.Name, arg.Bio)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
複製代碼
咱們須要在程序初始化時調用這個Prepare()
方法。
emit_interface
默認爲false
,設置該字段爲true
,會爲查詢結構生成一個接口。例如,在快速開始的示例中設置這個選項,最終生成的代碼會多出一個文件querier.go
:
// db/querier.go
type Querier interface {
CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error)
DeleteAuthor(ctx context.Context, id int64) error
DeleteAuthorN(ctx context.Context, id int64) (int64, error)
GetAuthor(ctx context.Context, id int64) (Author, error)
ListAuthors(ctx context.Context) ([]Author, error)
}
複製代碼
sqlc
在生成模型對象結構時會根據數據庫表的字段類型推算出一個 Go 語言類型,例如text
對應string
。咱們也能夠在配置文件中指定這種類型映射。
version: "1"
packages:
- name: "db"
path: "./db"
queries: "./query.sql"
schema: "./schema.sql"
overrides:
- go_type: "github.com/uniplaces/carbon.Time"
db_type: "pg_catalog.timestamp"
複製代碼
在overrides
下go_type
表示使用的 Go 類型。若是是非標準類型,必須指定全限定類型(即包路徑 + 類型名)。db_type
設置爲要映射的數據庫類型。sqlc
會自動導入對應的標準包或第三方包。生成代碼以下:
package db
import (
"github.com/uniplaces/carbon"
)
type Author struct {
ID int32
Name string
CreateAt carbon.Time
}
複製代碼
**須要注意的是db_type
的表示,文檔這裏一筆帶過,使用上仍是有些晦澀。**我也是看源碼才找到如何覆寫timestamp
類型的,須要將db_type
設置爲pg_catalog.timestamp
。同理timestamptz
、timetz
等類型也須要加上這個前綴。**通常複雜類型都須要加上前綴,通常的基礎類型能夠加也能夠不加。**遇到不肯定的狀況,能夠去看看源碼gen.go#L634。
也能夠設定某個字段的類型,例如咱們要將建立時間字段created_at
設置爲使用carbon.Time
:
version: "1"
packages:
- name: "db"
path: "./db"
queries: "./query.sql"
schema: "./schema.sql"
overrides:
- column: "authors.create_at"
go_type: "github.com/uniplaces/carbon.Time"
複製代碼
生成代碼以下:
// db/models.go
package db
import (
"github.com/uniplaces/carbon"
)
type Author struct {
ID int32
Name string
CreateAt carbon.Time
}
複製代碼
最後咱們還能夠給生成的結構字段命名:
version: "1"
packages:
- name: "db"
path: "./db"
queries: "./query.sql"
schema: "./schema.sql"
rename:
id: "Id"
name: "UserName"
create_at: "CreateTime"
複製代碼
上面配置爲生成的結構設置字段名,生成代碼:
package db
import (
"time"
)
type Author struct {
Id int32
UserName string
CreateTime time.Time
}
複製代碼
我以前使用 MySQL 較多。因爲sqlc
對 MySQL 的支持不太好,在體驗這個庫的時候仍是選擇支持較好的 PostgreSQL。不得不說,在 win10 上,PostgreSQL 的安裝門檻實在是過高了!我摸索了好久最後只能在www.enterprisedb.com/download-po…下載可執行文件。我選擇了 10.12 版本,下載、解壓、將文件夾中的bin
加入系統PATH
。建立一個data
目錄,而後執行下面的命令初始化數據:
$ initdb data
複製代碼
註冊 PostgreSQL 服務,這樣每次系統重啓後會自動啓動:
$ pg_ctl register -N "pgsql" -D D:\data
複製代碼
這裏的data
目錄就是上面建立的,而且必定要使用絕對路徑!
啓動服務:
$ sc start pgsql
複製代碼
最後使用咱們前面介紹的命令建立數據庫和表便可。
若是有使用installer
成功安裝的小夥伴,還請不吝賜教!
雖然目前還有一些不完善的地方,例如對 MySQL 的支持是實驗性的,sqlc
工具的確能大大簡化咱們使用 Go 編寫數據庫代碼的複雜度,提高咱們的編碼效率,減小咱們出錯的機率。使用 PostgreSQL 的小夥伴很是建議嘗試一番!
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
個人博客:darjun.github.io
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~