一、数据库表的设计
在项目过程中操作数据库尽量用mysql命令行进行操作,不过为了提高效率,在建表和改字段信息的时候可以借用可视化工具,查数据和字段用命令行。
整个项目共有八个数据表,user
表和advisor
表记录用户和顾问的model字段数据,orders
表、service
表和review
表分别记录订单、服务和订单评论三个实体的数据,adv_coin
表和user_coin
表用于记录用户和顾问的金币流水记录。以下是数据库设计中需要注意的几个地方:
1.主键Id的设计
从数据库理论而言,每个实体都要设计一个主键Id字段,用于唯一标识该实体的记录。例如用户表设置uid,顾问表设置aid,服务表设置sid。
对于id字段,一般情况下都会给自增,并且会设置自增起点。为了区分各个实体的记录,通常设置不同的区分性强的自增起点。例如在这个项目中,我给uid
设置自增起点为60000,aid为80000,sid为50000。对于数据体系庞大的项目,要考虑多位数的id设置。
2.字段的命名
数据库字段的命名采用下划线的方式,如果该字段与该实体是强相关的属性关系,就可以不用在前面加标识符。比如user
表中用户的姓名字段,不用命名为user_name
,直接用name
即可。但如果该字段与该实体没有属性关系,则要在前面加上标识符,例如orders
表中的用户名称,用user_name
会更合适。
3.字段的非空设置
一般情况下尽量保证表的字段都要求非空,除非该字段的数据确实可以忽略或者只在后续数据操作中添加。
例如订单表中的answer
字段,由于创建订单时没有问题的回答,只有相应顾问接单回复了才会有回答的数据,所以该字段不能设为非空。除类似情况,其他字段都设为了非空。
4.特殊字段处理
项目中经常会遇到一些枚举或者对格式有要求的字段。如性别字段在model
中设置了Int枚举,所以表中类型设为Int。如顾问的平均评分字段要求为一位小数且最大为10,类型就可用decimal(2,1)
。(decimal(x,y),x表示总位数,y标识小数点后位数)
二、在项目中配置数据库
设计完数据库后,接下来就要在项目中配置数据库了。通常情况下会把数据库的配置信息(端口号、用户名、密码)写在配置文件或者常量文件中,但由于本项目体量不大,我就直接写在了数据库初始化函数中。
首先,我们需要在项目中单独创建一个文件夹来放置数据库配置文件,我这里命名为common
,然后创建db.go
文件,在里面写数据库初始化和获取数据库指针的函数。
db.go :GO-GIN / common / db.go
package common
import (...)
var DB *sql.DB
func InitDB() *sql.DB {
scanner.SetTagName("key")
// 此处 root:password 为自己数据库的用户名:密码, demo为连接的数据库名, 3306为端口号(默认)
db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/demo?charset=utf8")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("数据库连接成功!")
}
DB = db
return db
}
func GetDB() *sql.DB {
return DB
}
配置好后,在main.go
文件中添加:
main.go :GO-GIN / main.go
func main() {
r := gin.Default()
// 初始化数据库
common.InitDB()
// 创建DB实例
db := common.GetDB()
// 延迟关闭数据库
defer db.Close()
// 初始化路由
r = router.InitRouter(r)
r.Run(":8000")
}
这样数据库的基本配置就完成了。
三、gendry的使用
Gendry是一个用于辅助操作数据库的Go包。基于go-sql-driver/mysql
,它提供了一系列的方法来为你调用标准库database/sql
中的方法准备参数。(官方链接跳转)
Gendery主要分为3个独立的部分:
1.manager:
主要用来初始化连接池(也就是sql.DB
对象),设置各种参数,因此叫manager
。你可以设置任何go-sql-driver/mysql
驱动支持的参数。
2.builder:
builder顾名思义,就是构建生成sql语句。手写sql虽然直观简单,但是可维护性差,最主要的是硬编码容易出错。builder不是一个ORM,它只是提供简单的API帮你生成sql语句。
3.scanner:
执行了数据库操作之后,要把返回的结果集和自定义的struct进行映射。Scanner提供一个简单的接口通过反射来进行结果集和自定义类型的绑定。scanner进行反射时会使用结构体的tag,默认使用的tagName是ddb:“xxx”,你也可以自定义。
本项目设置的scannar映射tag为“key”,在InitDB()函数中进行配置:
scanner.SetTagName("key")
对于某个字段,设置的key就是对应的数据表中的字段名。scanner函数会据此映射到struct上。
四、函数的封装与使用
函数封装习惯保存在单独文件夹中,此项目命名为utils
。创建gen.go
文件,保存gendry
语句的封装。
gen.go :GO-GIN / utils / gen.go
1.查找函数 - 通用 - 绑定结构体
import (
"database/sql"
"strconv"
...
qb "github.com/didi/gendry/builder"
"github.com/didi/gendry/scanner"
_ "github.com/go-sql-driver/mysql"
"go.uber.org/zap"
)
func GenSelectOne(obj interface{
}, table string, where map[string]interface{
}, selectField []string) error {
var db = common.GetDB()
cond, vals, _ := qb.BuildSelect(table, where, selectField)
rows, err := db.Query(cond, vals...)
if err != nil {
common.Log.Info(config.ErrSelect.Msg, zap.Error(err))
return err
}
defer rows.Close()
if err := scanner.Scan(rows, obj); err != nil {
common.Log.Info(config.ErrScan.Msg, zap.Error(err))
return err
}
return err
}
首先拿到数据库对象,然后使用gendry-builder
的BuildSelect
函数构建查询语句cond
和查询参数vals
,然后再用mysql
的原生查询语句db.Query
进行查询,得到结果 rows
。(注意:对rows操作完后要关闭该rows,具体原因见官网)。最后用gendry-scannar
中的Scan
函数,将结果绑定到传入的结构体obj
中。
2.更新函数 - 通用 - 绑定结构体
func GenUpdateNew(obj interface{
}, table string, where map[string]interface{
}) error {
var db = common.GetDB()
update := map[string]interface{
}{
}
//将传入的struct转map,用于更新
MapByReflect(update, obj)
cond, vals, _ := qb.BuildUpdate(table, where, update)
_, err := db.Exec(cond, vals...)
if err != nil {
common.Log.Info(config.ErrUpdate.Msg, zap.Error(err))
}
return err
}
更新函数中会将传入的struct
转换成一个map
,然后放到更新语句中来完成更新。转化函数会在后续的文章中详细讲解。
3.插入函数 - 通用 - 绑定结构体
// 插入
func GenInsertNew(obj interface{
}, table string) error {
var db = common.GetDB()
data := map[string]interface{
}{
}
var insertData []map[string]interface{
}
MapByReflect(data, obj)
insertData = append(insertData, data)
cond, vals, _ := qb.BuildInsert(table, insertData)
_, err := db.Exec(cond, vals...)
if err != nil {
common.Log.Info(config.ErrInsert.Msg, zap.Error(err))
}
return err
}
4.查找函数 - 定制 - 绑定结构体列表
// 多选 - 顾问列表
func GenSelectAdvisor(objArray *[]model.Advisor, table string, where map[string]interface{
}, selectField []string) error {
var db = common.GetDB()
cond, vals, _ := qb.BuildSelect(table, where, selectField)
rows, err := db.Query(cond, vals...)
if err != nil {
common.Log.Info(config.ErrSelect.Msg, zap.Error(err))
return err
}
defer rows.Close()
if err := scanner.Scan(rows, objArray); err != nil {
common.Log.Info(config.ErrScan.Msg, zap.Error(err))
return err
}
return err
}
当我们需要查询多个实体时,就要传入一个struct
数组。但经过多次尝试,发现无法统一定义入参为[]interface
,只能对入参和函数进行定制。上述查询函数为定制的查询顾问列表函数,传入一个顾问结构体数组并绑定。其他类似函数只需要修改一下入参的结构体类型即可。
5.函数的使用
user.go :GO-GIN / service / user.go
import (
"xxx/go-gin/common"
"xxx/go-gin/config"
"xxx/go-gin/model"
"xxx/go-gin/utils"
"go.uber.org/zap"
)
// 用户获取顾问列表
func GetAdvisorList(advisorList *[]model.Advisor) error {
where := map[string]interface{
}{
}
if err := utils.GenSelectAdvisor(advisorList, "advisor", where, config.AdvSelectAll); err != nil {
common.Log.Info(config.ErrSelect.Msg, zap.Error(err))
return err
}
return nil
}
数据库操作的语句使用在Service
层,只返回一个error
。在Service
层只需要对error
进行日志打印,然后返回到Controller
层就行。具体逻辑会在后续文章中详细展开讲。
五、总结与反思
数据库的设计和操作语句的编写封装是整个项目的基础,很多细节都需要注意,不然到后面又回来返工修改,效率会大大降低。
同时,数据流最好依赖于struct
结构体,把操作的数据与相应的struct
进行绑定,然后进行数据传递和下一步处理,这样比用map转来转去的效率高很多。