Skip to content

Latest commit

 

History

History
204 lines (135 loc) · 7.59 KB

File metadata and controls

204 lines (135 loc) · 7.59 KB

5.1 database/sql interface

Go doesn't provide any official database driver which PHP does, but it does have some database driver interface standards for developers develop database drivers. There is an advantage that if your code is developed according to these interface standards, you will not change any code when your database changes. Let's see what these database interface standards are.

sql.Register

This function is in package database/sql for registering database drivers when you use third-party database drivers. In all those drivers, they should call function Register(name string, driver driver.Driver) in init() function in order to register their drivers.

Let's take a look at corresponding code in drivers of mymysql and sqlite3:

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

//https://github.com/mikespook/mymysql driver
// 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)
}

We see that all third-party database drivers implemented this function to register themselves, and Go uses a map to save user drivers inside of databse/sql.

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

drivers[name] = driver

Therefore, this register function can register drivers as many as you want with all different name.

We always see following code when we use third-party drivers:

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

Here the blank _ is quite confusing for many beginners, and this is a great design in Go. We know this identifier is for discarding values from function return, and you have to use all imported packages in your code in Go. So when the blank is used with import means you need to execute init() function of that package without directly using it, which is for registering database driver.

driver.Driver

Driver is a interface that has a method Open(name string) that returns a interface of Conn.

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

This is a one-time Conn, which means it can be used only once in one goroutine. The following code will occur errors:

...
go goroutineA (Conn)  // query
go goroutineB (Conn)  // insert
...

Because Go has no idea about which goroutine does what operation, so the query operation may get result of insert, vice-versa.

All third-party drivers should have this function to parse name of Conn and return results correctly.

driver.Conn

This is a database connection interface with some methods, and as I said above, same Conn can only be used in one goroutine.

type Conn interface {
    Prepare(query string) (Stmt, error)
    Close() error
    Begin() (Tx, error)
}
  • Prepare returns prepare status of corresponding SQL commands for querying and deleting, etc.
  • Close closes current connection and clean resources. Most of third-party drivers implemented some kinds of connection pool, so you don't need to cache connections unless you want to have unexpected errors.
  • Begin returns a Tx that represents a affair handle, you can use it for querying, updating or affair roll back etc.

driver.Stmt

This is a ready status and is corresponding with Conn, so it can only be used in one goroutine like Conn.

type Stmt interface {
    Close() error
    NumInput() int
    Exec(args []Value) (Result, error)
    Query(args []Value) (Rows, error)
}
  • Close closes current connection, but it still returns rows data if it is doing query operation.
  • NumInput returns the number of obligate arguments, database drivers should check caller's arguments when the result is greater than 0, and it returns -1 when database drivers don't know any obligate argument.
  • Exec executes SQL commands of update/insert that are prepared in Prepare, returns Result.
  • Query executes SQL commands of select that are prepared in Prepare, returns rows data.

driver.Tx

Generally, affair handle only have submit or roll back, and database drivers only need to implement these two methods.

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

driver.Execer

This is an optional interface.

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

If the driver doesn't implement this interface, then when you call DB.Exec, it automatically calls Prepare and returns Stmt, then executes Exec of Stmt, then closes Stmt.

driver.Result

This is the interface for result of update/insert operations.

type Result interface {
    LastInsertId() (int64, error)
    RowsAffected() (int64, error)
}
  • LastInsertId returns auto-increment Id number after insert operation from database.
  • RowsAffected returns rows that affected after query operation.

driver.Rows

This is the interface for result set of query operation.

type Rows interface {
    Columns() []string
    Close() error
    Next(dest []Value) error
}
  • Columns returns fields information of database tables, the slice is one-to-one correspondence to SQL query field, not all fields in that database table.
  • Close closes Rows iterator.
  • Next returns next data and assigns to dest, all string should be converted to byte array, and gets io.EOF error if no more data available.

diriver.RowsAffected

This is a alias of int64, but it implemented Result interface.

type RowsAffected int64

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

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

driver.Value

This is a empty interface that can contain any kind of data.

type Value interface{}

The Value must be somewhat that drivers can operate or nil, so it should be one of following types:

int64
float64
bool
[]byte
string   [*] Except Rows.Next which cannot return string
time.Time

driver.ValueConverter

This defines a interface for converting normal values to driver.Value.

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

This is commonly used in database drivers and has many good features:

  • Convert driver.Value to corresponding database field type, for example convert int64 to uint16.
  • Convert database query results to driver.Value.
  • Convert driver.Value to user defined value in scan function.

driver.Valuer

This defines a interface for returning driver.Value.

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

Many types implemented this interface for conversion between driver.Value and itself.

At this point, you should have concepts about how to develop a database driver. Once you implement about interfaces for operations like add, delete, update, etc. There only left problems about communicating with specific database.

database/sql

databse/sql defines more high-level methods above database/sql/driver for more convenient operations with databases, and it suggests you to implement a connection pool.

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

As you can see, Open function returns a DB that has a freeConn, and this is the simple connection pool. Its implementation is very simple or ugly, it uses defer db.putConn(ci, err) in function Db.prepare to put connection into connection pool. Every time you call Conn function, it checks length of freeCoon, if it's greater than 0 means there is a reusable connection and directly returns to you, otherwise it creates a new connection and returns.

Links