Files
build-web-application-with-…/zh-tw/05.1.md
2019-02-26 01:40:54 +08:00

9.0 KiB
Raw Blame History

5.1 database/sql介面

Go與PHP不同的地方是Go官方沒有提供資料庫驅動而是為開發資料庫驅動定義了一些標準介面開發者可以根據定義的介面來開發相應的資料庫驅動這樣做有一個好處只要是按照標準介面開發的程式碼 以後需要遷移資料庫時不需要任何修改。那麼Go都定義了哪些標準介面呢讓我們來詳細的分析一下

sql.Register

這個存在於database/sql的函式是用來註冊資料庫驅動的當第三方開發者開發資料庫驅動時都會實現init函式在init裡面會呼叫這個Register(name string, driver driver.Driver)完成本驅動的註冊。

我們來看一下mymysql、sqlite3的驅動裡面都是怎麼呼叫的


//https://github.com/mattn/go-sqlite3驅動
func init() {
	sql.Register("sqlite3", &SQLiteDriver{})
}

//https://github.com/mikespook/mymysql驅動
// Driver automatically registered in database/sql
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
func init() {
	Register("SET NAMES utf8")
	sql.Register("mymysql", &d)
}

我們看到第三方資料庫驅動都是透過呼叫這個函式來註冊自己的資料庫驅動名稱以及相應的driver實現。在database/sql內部透過一個map來儲存使用者定義的相應驅動。


var drivers = make(map[string]driver.Driver)

drivers[name] = driver

因此透過database/sql的註冊函式可以同時註冊多個數據函式庫驅動只要不重複。

在我們使用database/sql介面和第三方函式庫的時候經常看到如下:

  import (
  	"database/sql"
   	_ "github.com/mattn/go-sqlite3"
  )

新手都會被這個_所迷惑其實這個就是Go設計的巧妙之處我們在變數賦值的時候經常看到這個符號它是用來忽略變數賦值的佔位符那麼套件引入用到這個符號也是相似的作用這兒使用_的意思是引入後面的套件名而不直接使用這個套件中定義的函式,變數等資源。

我們在2.3節流程和函式一節中介紹過init函式的初始化過程套件在引入的時候會自動呼叫套件的init函式以完成對套件的初始化。因此我們引入上面的資料庫驅動套件之後會自動去呼叫init函式然後在init函式裡面註冊這個資料庫驅動這樣我們就可以在接下來的程式碼中直接使用這個資料庫驅動了。

driver.Driver

Driver是一個數據函式庫驅動的介面他定義了一個method Open(name string)這個方法返回一個數據函式庫的Conn介面。


type Driver interface {
	Open(name string) (Conn, error)
}

返回的Conn只能用來進行一次goroutine的操作也就是說不能把這個Conn應用於Go的多個goroutine裡面。如下程式碼會出現錯誤


...
go goroutineA (Conn)  //執行查詢操作
go goroutineB (Conn)  //執行插入操作
...

上面這樣的程式碼可能會使Go不知道某個操作究竟是由哪個goroutine發起的,從而導致資料混亂比如可能會把goroutineA裡面執行的查詢操作的結果返回給goroutineB從而使B錯誤地把此結果當成自己執行的插入資料。

第三方驅動都會定義這個函式它會解析name引數來取得相關資料庫的連線資訊解析完成後它將使用此資訊來初始化一個Conn並返回它。

driver.Conn

Conn是一個數據函式庫連線的介面定義他定義了一系列方法這個Conn只能應用在一個goroutine裡面不能使用在多個goroutine裡面詳情請參考上面的說明。


type Conn interface {
	Prepare(query string) (Stmt, error)
	Close() error
	Begin() (Tx, error)
}

Prepare函式返回與當前連線相關的執行Sql語句的準備狀態可以進行查詢、刪除等操作。

Close函式關閉當前的連線執行釋放連線擁有的資源等清理工作。因為驅動實現了database/sql裡面建議的conn pool所以你不用再去實現快取conn之類別的這樣會容易引起問題。

Begin函式返回一個代表事務處理的Tx透過它你可以進行查詢,更新等操作,或者對事務進行回滾、遞交。

driver.Stmt

Stmt是一種準備好的狀態和Conn相關聯而且只能應用於一個goroutine中不能應用於多個goroutine。


type Stmt interface {
	Close() error
	NumInput() int
	Exec(args []Value) (Result, error)
	Query(args []Value) (Rows, error)
}

Close函式關閉當前的連結狀態但是如果當前正在執行queryquery還是有效返回rows資料。

NumInput函式返回當前預留引數的個數當返回>=0時資料庫驅動就會智慧檢查呼叫者的引數。當資料庫驅動套件不知道預留引數的時候返回-1。

Exec函式執行Prepare準備好的sql傳入引數執行update/insert等操作返回Result資料

Query函式執行Prepare準備好的sql傳入需要的引數執行select操作返回Rows結果集

driver.Tx

事務處理一般就兩個過程,遞交或者回滾。資料庫驅動裡面也只需要實現這兩個函式就可以


type Tx interface {
	Commit() error
	Rollback() error
}

這兩個函式一個用來遞交一個事務,一個用來回滾事務。

driver.Execer

這是一個Conn可選擇實現的介面


type Execer interface {
	Exec(query string, args []Value) (Result, error)
}

如果這個介面沒有定義那麼在呼叫DB.Exec,就會首先呼叫Prepare返回Stmt然後執行Stmt的Exec然後關閉Stmt。

driver.Result

這個是執行Update/Insert等操作返回的結果介面定義


type Result interface {
	LastInsertId() (int64, error)
	RowsAffected() (int64, error)
}

LastInsertId函式返回由資料庫執行插入操作得到的自增ID號。

RowsAffected函式返回query操作影響的資料條目數。

driver.Rows

Rows是執行查詢返回的結果集介面定義


type Rows interface {
	Columns() []string
	Close() error
	Next(dest []Value) error
}

Columns函式返回查詢資料庫表的欄位資訊這個返回的slice和sql查詢的欄位一一對應而不是返回整個表的所有欄位。

Close函式用來關閉Rows迭代器。

Next函式用來返回下一條資料把資料賦值給dest。dest裡面的元素必須是driver.Value的值除了string返回的資料裡面所有的string都必須要轉換成[]byte。如果最後沒資料了Next函式最後返回io.EOF。

driver.RowsAffected

RowsAffected其實就是一個int64的別名但是他實現了Result介面用來底層實現Result的表示方式


type RowsAffected int64

func (RowsAffected) LastInsertId() (int64, error)

func (v RowsAffected) RowsAffected() (int64, error)

driver.Value

Value其實就是一個空介面他可以容納任何的資料


type Value interface{}

drive的Value是驅動必須能夠操作的ValueValue要麼是nil要麼是下面的任意一種


int64
float64
bool
[]byte
string   [*]除了Rows.Next返回的不能是string.
time.Time

driver.ValueConverter

ValueConverter介面定義瞭如何把一個普通的值轉化成driver.Value的介面


type ValueConverter interface {
	ConvertValue(v interface{}) (Value, error)
}

在開發的資料庫驅動套件裡面實現這個介面的函式在很多地方會使用到這個ValueConverter有很多好處

  • 轉化driver.value到資料庫表相應的欄位例如int64的資料如何轉化成資料庫表uint16欄位
  • 把資料庫查詢結果轉化成driver.Value值
  • 在scan函式裡面如何把driver.Value值轉化成使用者定義的值

driver.Valuer

Valuer介面定義了返回一個driver.Value的方式


type Valuer interface {
	Value() (Value, error)
}

很多型別都實現了這個Value方法用來自身與driver.Value的轉化。

透過上面的講解,你應該對於驅動的開發有了一個基本的瞭解,一個驅動只要實現了這些介面就能完成增刪查改等基本操作了,剩下的就是與相應的資料庫進行資料互動等細節問題了,在此不再贅述。

database/sql

database/sql在database/sql/driver提供的介面基礎上定義了一些更高階的方法用以簡化資料庫操作,同時內部還建議性地實現一個conn pool。


type DB struct {
	driver 	 driver.Driver
	dsn    	 string
	mu       sync.Mutex // protects freeConn and closed
	freeConn []driver.Conn
	closed   bool
}

我們可以看到Open函式返回的是DB物件裡面有一個freeConn它就是那個簡易的連線池。它的實現相當簡單或者說簡陋就是當執行db.prepare -> db.prepareDC的時候會defer dc.releaseConn,然後呼叫db.putConn,也就是把這個連線放入連線池,每次呼叫db.conn的時候會先判斷freeConn的長度是否大於0大於0說明有可以複用的conn直接拿出來用就是了如果不大於0則建立一個conn然後再返回之。