Go 每日一庫之 sqlc

簡介

在 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-startedsqlc爲咱們在同級目錄下生成了數據庫操做代碼,目錄結構以下:windows

db
├── db.go
├── models.go
└── query.sql.go
複製代碼

sqlc根據咱們schema.sqlquery.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) 複製代碼

其中Queriessqlc封裝的一個結構。

說了這麼多,來看看如何使用:

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 語句中,建表時有兩列idbirth_year。第一條ALTER TABLE語句添加了一列bio,第二條刪除了birth_year列,第三條將表名authors改成writerssqlc依據最終的表名writers和表中的列idbio生成代碼:

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"
複製代碼

overridesgo_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。同理timestamptztimetz等類型也須要加上這個前綴。**通常複雜類型都須要加上前綴,通常的基礎類型能夠加也能夠不加。**遇到不肯定的狀況,能夠去看看源碼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
}
複製代碼

安裝 PostgreSQL

我以前使用 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😄

參考

  1. sqlc GitHub:github.com/kyleconroy/…
  2. Go 每日一庫 GitHub:github.com/darjun/go-d…

個人博客:darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索