【七天从零实现ORM框架】Day02:database/sql基础

memo012 · · 407 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210119120924737.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMDY2MDY2,size_16,color_FFFFFF,t_70#pic_center) 今天是TORM框架的编写的第一天,主要完成几个任务: - Go-SQL-Driver驱动的简单使用 - 使用 Go 语言标准库 database/sql 连接并操作 MySQL 数据库,并简单封装。 >源代码:在【迈莫coding】中回复关键字「 torm 」获取github地址链接。 后续会为【七天从零实现TORM框架】录制视频,文章+视频+代码 # 驱动选型 Go支持的MYSQL的驱动种类目前非常多,在这里我查阅资料,列出几种mysql驱动,他有些是支持database/sql标准,但有些是采用了自己的实现接口,常见的有以下几种: - [https://github.com/Go-SQL-Driver/MySQL](https://github.com/Go-SQL-Driver/MySQL) :完全支持database/sql标准,并且是使用go语言写的 - [https://github.com/ziutek/mymysql](https://github.com/ziutek/mymysql) :支持database/sql,也支持自定义接口,也是完全用go语言写的 - [https://github.com/Philio/GoMySQL](https://github.com/Philio/GoMySQL) :不支持database/sql,完全使用go语言编写 在本项目中的话,我们采用第一种方式作为mysql驱动,在此也推荐给大家,主要原因: - 完全支持database/sql接口 - 支持keepalive,保持长连接和连接稳定 - 维护频率高 # 安装Go-SQL-Driver 执行下面命令: ```go go get -u github.com/go-sql-driver/mysql ``` # Go-SQL-Driver驱动实战 Go语言中提供了标准库 database/sql 用来与数据库进行交互,接下来简单熟悉以下 Go-SQL-Driver 的使用。 ## DML操作:增删改 他有两种操作方式,一种是使用 **Exec 函数**添加,一种是首先使用 **Prepare 获得stmt**,然后调用 Exec 函数添加。 - **使用 Exec 函数添加** ```go func (db *DB) Exec(query string, args ...interface{}) (Result, error) ``` 示例代码: ```go result, err := db.Exec("select * from user where user_name = ?", "迈莫coding") ``` - **首先使用 Prepare 获得stmt,然后调用 Exec 函数添加** ```go func (db *DB) Prepare(query string) (*Stmt, error) { return db.PrepareContext(context.Background(), query) } ``` 示例代码: ```go stmt,err:=db.Prepare("INSERT INTO user(user_name, age) values(?,?)") //补充完整sql语句,并执⾏ result,err:=stmt.Exec("迈莫coding",1) ``` 预编译语句(Prepared Statement) 提供了诸多好处,因此我们在开发中尽量使⽤它。下⾯列出了使⽤预编译语句所提供的功能: - `PreparedStatement` 可以实现自定义参数的查询 - `PreparedStatement` 通常⽐手动拼接字符串SQL 语句高效 - `PreparedStatement` 可以防⽌SQL注⼊攻击 ## DQL操作:查询 **单条语句查询** ```go func (db *DB) QueryRow(query string, args ...interface{}) *Row ``` 示例代码: ```go var userName string var age int err := db.QueryRow("select user_name,age from user where user_name = ?", "迈莫coding") err := row.Scan(&userName, &age) ``` **多条语句查询** Query 获取数据, for xxx.Next() 遍历数据: ⾸先使用Query()方法进⾏查询,如果查询无误,返回Rows,就是所有行的信息,类似结果集。 ```go func (db *DB) Query(query string, args ...interface{}) (*Rows, error) ``` 我们可以通过 Next() 方法判断是否存在下一条数据,如果有,可以使⽤之前的 Scan() ⽅法读取一行,然后继续判断,继续获取。这个过程的本质就是迭代,所以通常要配合循环使⽤用。 ```go func (rs *Rows) Next() bool ``` 每次 db.Query 操作后,都建议调用 rows.Close() 。 因为 db.Query() 会从数据库连接池中获取⼀个连接, 这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时,会发⽣生⼀个内部EOF错误,自动调⽤ rows.Close() ,但如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭,则此连接会一直被占用。 因此通常我们使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中。 # 核心底层结构Session 首先先用 Go-SQL-Driver 驱动实现与数据库的交互,也是TORM最终要与数据库交互的底层代码。今天就要实现通过原始SQL语句与数据库进行交互。代码存放在根目录下 raw.go 文件中。 ```go raw.go package session import ( "database/sql" _ "github.com/go-sql-driver/mysql" log "github.com/sirupsen/logrus" "strings" ) // session类 旨于与数据库进行交互 type Session struct { // 数据库引擎 db *sql.DB // 事务 tx *sql.Tx // SQL动态参数 sqlValues []interface{} // SQL语句 sql strings.Builder } // DB return tx if a tx begins otherwise return *sql.DB func (s *Session) DB() CommonDB { if s.tx != nil { return s.tx } return s.db } // 实例化 Session func NewSession(db *sql.DB) *Session { return &Session{db: db} } func (s *Session) Clear() { s.sql.Reset() s.sqlValues = nil } func (s *Session) Raw(sql string, values ...interface{}) *Session { s.sql.WriteString(sql) s.sql.WriteString(" ") s.sqlValues = append(s.sqlValues, values...) return s } ``` **代码说明:** - 第13~22行:创建一个session结构体,作用是torm框架底层与数据库交互。其中包含几个成员变量,解释如下: - db: 数据库引擎 - tx: 数据库事务 - sqlValues:用于SQL语句中参数的存放位置 - sql:SQL语句的存放位置 - 第25~30行:获取数据库引擎 - 第33~35行:实例化session对象 - 第36~39行:当每次执行完一次SQL操作后,会将sql语句和sql参数清空掉 - 第41行~46行: Raw() 函数用户将SQL语句和请求参数写入到 session 结构体中的 sql 和 sqlValues 字段中 接下来的话,需要进行封装增删改查操作函数, **Exec()** 、 **QueryRow()** 、 **Query()** 这三个函数。 ```go raw.go // 定义统一接口 便于后续支持多个数据引擎 type CommonDB interface { Query(query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row Exec(query string, args ...interface{}) (sql.Result, error) } var _ CommonDB = (*sql.DB)(nil) var _ CommonDB = (*sql.Tx)(nil) // 增删改操作 func (s *Session) Exec() (result sql.Result, err error) { defer s.Clear() log.Info(s.sql.String(), s.sqlValues) if result, err = s.DB().Exec(s.sql.String(), s.sqlValues...); err != nil { log.Error(err) } return } // 单条查询操作 func (s *Session) QueryRow() *sql.Row { defer s.Clear() log.Info(s.sql.String(), s.sqlValues) return s.DB().QueryRow(s.sql.String(), s.sqlValues...) } // 多条查询操作 func (s *Session) Query() (rows *sql.Rows, err error) { defer s.Clear() log.Info(s.sql.String(), s.sqlValues) if rows, err = s.DB().Query(s.sql.String(), s.sqlValues...); err != nil { log.Error(err) } return } ``` **代码说明:** - 第4~8行:定义统一接口(增删改查通用操作方法),便于后续适应不同的数据库引擎 - 第10~11行:用户检测 sql.DB 类型和 sql.Tx 类型是否实现了接口 Common 中的所有方法,若未全部实现,则在编译期间报错。 - 第14~21行:增删改通用函数封装 - 第24~28行:单条SQL语句查询函数封装 - 第31行~38行:多条SQL语句查询函数封装 到这里,与数据库交互的功能(增删改查)也基本完成,现在进入测试阶段。 # 测试 TORM的单元测试比较完善,没写完一个模块,都会编写单元测试代码,由于咱们编写完了 raw.go 与数据库交互的代码,那么咱们来进行测试是否可行,代码存放在根目录下 raw_test.go 文件中。 ```go raw_test.go package session import ( "database/sql" "os" "testing" log "github.com/sirupsen/logrus" ) var db *sql.DB func TestMain(m *testing.M) { db, _ = sql.Open("mysql", "root:12345678@tcp(127.0.0.1:3306)/po?charset=utf8mb4") code := m.Run() _ = db.Close() os.Exit(code) } func New() *Session { return NewSession(db) } func TestSession_QueryRow(t *testing.T) { s := New() var userName string var age int s = s.Raw("select user_name,age from user where user_name = ?", "迈莫") res := s.QueryRow() if err := res.Scan(&userName, &age); err != nil { t.Fatal("failed to query db", err) } log.Info("userName--", userName) log.Info("age--", age) } func TestSession_Exec(t *testing.T) { s := New() key := "迈莫" s = s.Raw("insert into user(user_name, age) values(?, ?)", key, 22) _, err := s.Exec() if err != nil { t.Fatal("failed to insert db", err) } } func TestSession_Query(t *testing.T) { s := New() var userName string var age int s = s.Raw("select user_name, age from user") rows, err := s.Query() if err != nil { t.Fatal("fialed to query db", err) } for rows.Next() { err = rows.Scan(&userName, &age) if err != nil { t.Fatal("fialed to query db", err) } log.Info("userName--", userName) log.Info("age--", age) } } ``` **测试结果:** - `TestSession_QueryRow(t *testing.T)` 方法测试结果 ```go === RUN TestSession_QueryRow time="2021-01-15T12:29:57+08:00" level=info msg="select user_name,age from user where user_name = ? [迈莫]" time="2021-01-15T12:29:57+08:00" level=info msg="userName--迈莫" time="2021-01-15T12:29:57+08:00" level=info msg=age--1 --- PASS: TestSession_QueryRow (0.00s) PASS TestSession_Exec(t *testing.T) 方法测试结果 === RUN TestSession_Exec time="2021-01-15T12:32:51+08:00" level=info msg="insert into user(user_name, age) values(?, ?) [迈莫 21]" --- PASS: TestSession_Exec (0.00s) PASS ``` - `TestSession_Query(t *testing.T)` 方法测试结果 ```go time="2021-01-15T12:33:35+08:00" level=info msg="select user_name, age from user []" time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫coding" time="2021-01-15T12:33:35+08:00" level=info msg=age--1 time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫" time="2021-01-15T12:33:35+08:00" level=info msg=age--1 time="2021-01-15T12:33:35+08:00" level=info msg="userName--迈莫" time="2021-01-15T12:33:35+08:00" level=info msg=age--21 --- PASS: TestSession_Query (0.00s) ``` # 代码目录 ```go torm |--raw.go // 底层与数据库交互语句 |--raw_test.go |--go.mod ``` 到这里,第一天的任务就编写完成了。回顾一下,第一天主要完成了与数据库交互的代码,也就是 raw.go 代码编写,是TORM框架中与数据库交互的核心功能。 >文章也会持续更新,可以微信搜索「 迈莫coding 」第一时间阅读,回复『1024』领取学习go资料。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210119121305613.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMDY2MDY2,size_16,color_FFFFFF,t_70#pic_center)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

407 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传