在 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
:json
name
:生成的包名;path
:生成文件的路徑;queries
:查詢 SQL 文件;schema
:建表 SQL 文件。在 windows 上執行下面的命令生成對應的 Go 代碼:windows
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
爲咱們在同級目錄下生成了數據庫操做代碼,目錄結構以下:
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 的安裝門檻實在是過高了!我摸索了好久最後只能在https://www.enterprisedb.com/download-postgresql-binaries下載可執行文件。我選擇了 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😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~