基于mysql的数据库基本操作及底层原理的简单解析
本文通过介绍面向mysql数据库的基本操作,包括;建库建表以及最基本的增删查改,来简单探究database/sql以及github.com/go-sql-driver/mysql的实现原理。
1. 创建数据库
使用Open()初始化相关的资源和配置,在使用Open()时不需要指定数据库的名字(因为此时还没有数据库),代码如下:
代码块一
1 import ( 2 "database/sql" 3 "fmt" 4 5 _ "github.com/go-sql-driver/mysql" 6 ) 7 ... 8 var err error 9 sqlStat := fmt.Sprintf("%s:%s@(%s)/", "root", "123", "127.0.0.1:3306") 10 dbtmp, err := sql.Open("mysql", sqlStat) 11 if err != nil { 12 panic(err) 13 }
那Open()在底层做了什么,让我们来一探究竟。
代码块二
1 func Open(driverName, dataSourceName string) (*DB, error) { 2 driversMu.RLock() 3 driveri, ok := drivers[driverName] 4 driversMu.RUnlock() 5 if !ok { 6 return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName) 7 } 8 9 if driverCtx, ok := driveri.(driver.DriverContext); ok { 10 connector, err := driverCtx.OpenConnector(dataSourceName) 11 if err != nil { 12 return nil, err 13 } 14 return OpenDB(connector), nil 15 } 16 17 return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil 18 }
如上,首先从数据库驱动缓存里面取出mysql对应的驱动器,读者可能会问到:此驱动缓存是何时将各种数据库的驱动添加进去的呢?
其实它不是由于sql包主动添加的,而是sql包对外提供了一个Register()接口,用于注册数据库驱动,代码如下:
代码块三
1 func Register(name string, driver driver.Driver) { 2 driversMu.Lock() 3 defer driversMu.Unlock() 4 if driver == nil { 5 panic("sql: Register driver is nil") 6 } 7 if _, dup := drivers[name]; dup { 8 panic("sql: Register called twice for driver " + name) 9 } 10 drivers[name] = driver 11 }
那mysql是何时注册自己的数据库驱动的呢?我们看到代码块一的第5行,在包名之前添加下划线的作用是只执行该包的init方法,而不需要导入整个包。关于Go语言程序模块的初始化顺序,参见https://studygolang.com/articles/6464。
那我们再来看看mysql数据库驱动包中的init方法做了什么:
1 func init() { 2 sql.Register("mysql", &MySQLDriver{}) 3 }
果然如此,mysql数据库驱动包在初始化的时候向sql包注册了自己的驱动。
我们回到代码块二,在第9行,直接将driveri断言成了DriverContext接口类型,这里driveri虽然是Driver类型,但是由上述分析可知其动态值是MySQLDriver类型,根据Go语言特性,一个接口类型可直接断言成另外一个接口类型,那么其动态类型必须实现后者,所以我们看看MySQLDriver是否实现了DriverContext接口:
代码块四
1 // If a Driver implements DriverContext, then sql.DB will call 2 // OpenConnector to obtain a Connector and then invoke 3 // that Connector's Conn method to obtain each needed connection, 4 // instead of invoking the Driver's Open method for each connection. 5 // The two-step sequence allows drivers to parse the name just once 6 // and also provides access to per-Conn contexts. 7 type DriverContext interface { 8 // OpenConnector must parse the name in the same format that Driver.Open 9 // parses the name parameter. 10 OpenConnector(name string) (Connector, error) 11 }
代码块五
1 // OpenConnector implements driver.DriverContext. 2 func (d MySQLDriver) OpenConnector(dsn string) (driver.Connector, error) { 3 cfg, err := ParseDSN(dsn) 4 if err != nil { 5 return nil, err 6 } 7 return &connector{ 8 cfg: cfg, 9 }, nil 10 }
答案YES。我们从代码块四的解释中可以大致知道DriverContext接口的作用是让不同数据库驱动包提供工厂方法(OpenConnector)来获取其实现的Connector接口实例。而Connector接口定义如下:
代码块六
1 // A Connector represents a driver in a fixed configuration 2 // and can create any number of equivalent Conns for use 3 // by multiple goroutines. 4 // 5 // A Connector can be passed to sql.OpenDB, to allow drivers 6 // to implement their own sql.DB constructors, or returned by 7 // DriverContext's OpenConnector method, to allow drivers 8 // access to context and to avoid repeated parsing of driver 9 // configuration. 10 type Connector interface { 11 // Connect returns a connection to the database. 12 // Connect may return a cached connection (one previously 13 // closed), but doing so is unnecessary; the sql package 14 // maintains a pool of idle connections for efficient re-use. 15 // 16 // The provided context.Context is for dialing purposes only 17 // (see net.DialContext) and should not be stored or used for 18 // other purposes. 19 // 20 // The returned connection is only used by one goroutine at a 21 // time. 22 Connect(context.Context) (Conn, error) 23 24 // Driver returns the underlying Driver of the Connector, 25 // mainly to maintain compatibility with the Driver method 26 // on sql.DB. 27 Driver() Driver 28 }
从解释中可以看出Connector接口主要用于与mysql server创建连接。而在代码块五中工厂方法OpenConnector确实返回了一个实现了Connector接口的结构体connector实例。
再次回到代码块二,接下来调用OpenDB(),如下:
1 func OpenDB(c driver.Connector) *DB { 2 ctx, cancel := context.WithCancel(context.Background()) 3 db := &DB{ 4 connector: c, 5 openerCh: make(chan struct{}, connectionRequestQueueSize), 6 resetterCh: make(chan *driverConn, 50), 7 lastPut: make(map[*driverConn]string), 8 connRequests: make(map[uint64]chan connRequest), 9 stop: cancel, 10 } 11 12 go db.connectionOpener(ctx) 13 go db.connectionResetter(ctx) 14 15 return db 16 }
上述代码先是初始化配置和资源,然后启动一个goroutine调用connectionOpener()来监听新建连接的请求。
总结:所以Open()的作用主要是初始化配置和资源,并监听新建连接的请求。
调用了Open()之后,接着调用Exec()执行sql语句创建数据库,代码如下:
1 sqlStat = fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", "mydb") 2 _, err = dbtmp.Exec(sqlStat) 3 if err != nil { 4 panic(err) 5 }
如果追踪下去,会发现实际上是从连接池选取一条连接或者新建一条的连接,将sql语句发送过去执行。
由于前面调用Open()时没有指定具体的数据库,因此底层与mysql server建立连接时并不会指定使用具体哪个数据库,所以应用程序后续进行数据库操作时(如CRUD),必须首先执行如下语句,用于与mysql server建立指定具体数据库的连接:
1 sql = fmt.Sprintf("USE %s", DBName) 2 _, err = db.Exec(sql)
这样比较麻烦,所以我们在创建数据库之后,可以重新创建一个DB实例,同时指定刚刚创建的数据库的名字。代码如下:
1 sqlStat = fmt.Sprintf("%s:%s@(%s)/%s", "root", "123", "127.0.0.1:3306", "mydb") 2 db, err = sql.Open("mysql", sqlStat) 3 if err != nil { 4 panic(err) 5 }
而返回的DB实例通常作为全局变量在程序的整个生命周期用于对数据库的操作。
2. CRUD
这一部分内容为对数据库表记录最基本的增删查改操作。
1)添加记录
1 /** 2 * Insert single record of 'tableName' 3 * @params: value is the record to Insert(field name and value pairs) 4 * @return: error info 5 */ 6 7 func Insert(tableName string, value map[string]interface{}) error { 8 sqlStr := "INSERT " + tableName + " SET" 9 var args []interface{} 10 for k, v := range value { 11 sqlStr = sqlStr + " " + k + "=?," 12 args = append(args, v) 13 } 14 //remove the last ',' 15 sqlArr := []byte(sqlStr) 16 sqlStr = string(sqlArr[:len(sqlArr)-1]) 17 18 // prepare 19 stmt, err := db.Prepare(sqlStr) 20 if err != nil { 21 logger.Error("Prepare sql error: ", err.Error()) 22 return fmt.Errorf("Insert data failed.") 23 } 24 // exec 25 _, err = stmt.Exec(args...) 26 if err != nil { 27 logger.Error("Exec sql error: ", err.Error()) 28 return fmt.Errorf("Insert data failed.") 29 } 30 31 return nil 32 }
2)删除记录
1 /** 2 * Delete record of 'tableName' 3 * @params: condition is used to find the records to be deleted, and is table field name-value pairs 4 * @return: error info 5 */ 6 7 func Delete(tableName string, condition map[string]interface{}) error { 8 // join sql statement 9 sqlStr := "DELETE FROM " + tableName + " WHERE" 10 var args []interface{} 11 for k, v := range condition { 12 sqlStr = sqlStr + " " + k + "=?," 13 args = append(args, v) 14 } 15 //remove the last ',' 16 sqlArr := []byte(sqlStr) 17 sqlStr = string(sqlArr[:len(sqlArr)-1]) 18 19 // prepare 20 stmt, err := db.Prepare(sqlStr) 21 defer stmt.Close() 22 if err != nil { 23 logger.Error("Prepare sql error: ", err.Error()) 24 return fmt.Errorf("Delete data failed") 25 } 26 // exec 27 _, err = stmt.Exec(args...) 28 if err != nil { 29 logger.Error("Exec sql error: ", err.Error()) 30 return fmt.Errorf("Delete data failed") 31 } 32 33 return nil 34 }
3)查询记录(可查多条记录)
1 /** 2 * Query records of 'tableName' 3 * @params: condition is used to find the records, and is table field name-value pairs 4 * @return: query result records encapsulated into a map(for now, all values' type are string) 5 * error info 6 */ 7 8 func Query(tableName string, condition map[string]interface{}) ([]map[string]interface{}, error) { 9 // join sql statement 10 sqlStr := "SELECT * FROM " + tableName 11 var args []interface{} 12 if len(condition) != 0 { 13 sqlStr += " WHERE" 14 for k, v := range condition { 15 sqlStr = sqlStr + " " + k + "=? AND" 16 args = append(args, v) 17 } 18 //remove the last 'and' 19 sqlArr := []byte(sqlStr) 20 sqlStr = string(sqlArr[:len(sqlArr)-3]) 21 } 22 // query 23 rows, err := db.Query(sqlStr, args...) 24 if err != nil { 25 logger.Error("Query error: ", err.Error()) 26 return nil, fmt.Errorf("Query data failed") 27 } 28 defer rows.Close() 29 cols, err := rows.Columns() //get table fields name 30 if err != nil { 31 logger.Error("Get columns error: ", err.Error()) 32 return nil, fmt.Errorf("Query data failed") 33 } 34 colsLen := len(cols) 35 var queryResult []map[string]interface{} //query result that may include multiple records. 36 for rows.Next() { 37 beans := make([]interface{}, colsLen) 38 for i := 0; i < colsLen; i++ { 39 beans[i] = &sql.NullString{} 40 } 41 err := rows.Scan(beans...) 42 if err != nil { 43 logger.Error("Scan record from table error: ", err.Error()) 44 } 45 queryResult = append(queryResult, toMap(beans, cols)) 46 } 47 48 return queryResult, nil 49 }
需要解释的是,我把查询出来的记录转化成了map切片,切片的每个元素为一条记录,map的key为数据库表中的字段名,value为值,这里有个缺陷就是value都转换成了string类型。
4)更改记录(只支持最简单的UPDATE语句,即为:UPDATE [table name] SET [fields name and value to be changed] WHERE [condition fileds name and value])
1 /** 2 * update record of 'tableName' 3 * @params: condition is used to find the records 4 * value includes names and values of the fields to be updated 5 * @return: error info 6 */ 7 8 func Update(tableName string, condition, value map[string]interface{}) error { 9 // join sql statement 10 sqlStr := "UPDATE " + tableName + " SET" 11 var args []interface{} 12 for k, v := range value { 13 sqlStr = sqlStr + " " + k + "=?," 14 args = append(args, v) 15 } 16 //remove the last ',' 17 sqlArr := []byte(sqlStr) 18 sqlStr = string(sqlArr[:len(sqlArr)-1]) 19 sqlStr += " WHERE" 20 for k, v := range condition { 21 sqlStr = sqlStr + " " + k + "=? AND" 22 args = append(args, v) 23 } 24 //remove the last 'and' 25 sqlArr = []byte(sqlStr) 26 sqlStr = string(sqlArr[:len(sqlArr)-3]) 27 // prepare 28 stmt, err := db.Prepare(sqlStr) 29 if err != nil { 30 logger.Error("Prepare sql error: ", err.Error()) 31 return fmt.Errorf("Update data failed") 32 } 33 // exec 34 _, err = stmt.Exec(args...) 35 if err != nil { 36 logger.Error("Exec sql error: ", err.Error()) 37 return fmt.Errorf("Update data failed") 38 } 39 40 return nil 41 }

浙公网安备 33010602011771号