Go sqlx 框架使用下划线命名法处理结构体字段

avtion · · 1021 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

## 关于 sqlx sqlx 是一款 Golang 数据库操作库,它基于原生库 `database/sql` 上提供了一组扩展 API,是原生库 API 的超集。 默认情况下,sqlx 在进行结构体解析和 scan 时会获取对应的数据库字段,优先级如下: 1. 若 tag 存在 db 标签,根据 tag 获取数据库对应字段名,源码可见 [sqlx.go#43](https://github.com/jmoiron/sqlx/blob/master/sqlx.go#L43)。 2. 若 tag 不存在 db 标签,将字段名全部小写作为数据库对应字段名,源码可见 [sqlx.go#27](https://github.com/jmoiron/sqlx/blob/master/sqlx.go#L27)。 根据 [TiDB SQL 开发规范及基本原则 - 对象命名规范](https://docs.pingcap.com/zh/appdev/dev/basic-principles#%E5%AF%B9%E8%B1%A1%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83) 中「命名建议使用具有意义的英文词汇,词汇中间以下划线分隔」的内容,我们需要将第二种情况「全部小写」改为「下划线命名法」。 修改 sqlx 数据库字段生成函数一共需要两步。 1. 编写下划线命名法 NameMapper 函数。 2. 修改默认 NameMapper 函数。 ## 编写下划线命名法 NameMapper 函数 根据源码,我们可知默认情况下,sqlx 使用的是 `strings.ToLower` 原生库 strings 的方法进行字段处理,该函数仅有名为 name,类型为 string 的参数。 接下来,我们可以参考 `gorm` 的源码(gorm 默认情况下使用下划线命名法进行数据库字段生成)提取出 `toDBName` 函数,详细可见 [gorm - schema/naming.go:117](https://github.com/go-gorm/gorm/blob/master/schema/naming.go#L117)。 以下是笔者提出的下划线命名法字段生成函数 `toDBName` : ```go import "strings" var commonInitialismsReplacer = newCommonInitialismsReplacer() func newCommonInitialismsReplacer() *strings.Replacer { // https://github.com/golang/lint/blob/master/lint.go#L770 commonInitialisms := []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UID", "UI", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"} commonInitialismsForReplacer := make([]string, 0, len(commonInitialisms)) for _, initialism := range commonInitialisms { commonInitialismsForReplacer = append(commonInitialismsForReplacer, initialism, strings.Title(strings.ToLower(initialism))) } replacer := strings.NewReplacer(commonInitialismsForReplacer...) return replacer } func toDBName(name string) string { if name == "" { return "" } var ( value = commonInitialismsReplacer.Replace(name) buf strings.Builder lastCase, nextCase, nextNumber bool // upper case == true curCase = value[0] <= 'Z' && value[0] >= 'A' ) for i, v := range value[:len(value)-1] { nextCase = value[i+1] <= 'Z' && value[i+1] >= 'A' nextNumber = value[i+1] >= '0' && value[i+1] <= '9' if curCase { if lastCase && (nextCase || nextNumber) { buf.WriteRune(v + 32) } else { if i > 0 && value[i-1] != '_' && value[i+1] != '_' { buf.WriteByte('_') } buf.WriteRune(v + 32) } } else { buf.WriteRune(v) } lastCase = curCase curCase = nextCase } if curCase { if !lastCase && len(value) > 1 { buf.WriteByte('_') } buf.WriteByte(value[len(value)-1] + 32) } else { buf.WriteByte(value[len(value)-1]) } ret := buf.String() return ret } ``` ## 修改默认 NameMapper 函数 根据 sqlx 源码,我们可知 `NameMapper` 是 sqlx 的一个全局变量,所以我们只需要在程序初始化过程时修改变量即可。 ```go func init() { sqlx.NameMapper = toDBName } ``` ## 效果展示 Demo 将会展示使用全部小写和替换下划线命名法函数的区别。 值得注意的是,测试环境使用 golang v1.17 、sqlx v1.3.4 以及 TiDB v1.6.1。 1. 首先我们需要创建一个数据表 testTable 进行演示。 ```sql CREATE TABLE `test_table` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(256) NOT NULL, `sex` varchar(256) NOT NULL, `counter` bigint(20) NOT NULL, `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `miss_counter` bigint(20) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `test_table_name_uindex` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=30001 ``` 2. 接下来在 Go 程序中定义一个名为 testTable 的结构体,其中 `MissCounter` 字段会受到 NameMapper 的影响。 ```sql type testTable struct { ID uint64 Name string // unique index Sex string Counter uint64 // 命中计数字段 MissCounter uint64 // 非命中计数字段 CreateAt time.Time UpdateAt time.Time } ``` 3. 接下来我们需要编写一段插入程序代码,以此来演示不同数据库字段生成函数的影响。 ```go func main(){ tx, err := db.BeginTxx(ctx, &sql.TxOptions{}) if err != nil { log.Error(err) return } const insertSQL = "INSERT INTO test_table (name, sex, counter, miss_counter) VALUES (:name, :sex, :counter, :miss_counter);" stmt, errPrepare := tx.PrepareNamedContext(ctx, insertSQL) if errPrepare != nil { log.Errorf("failed to prepare, err: %v", errPrepare) _ = tx.Rollback() return } for _, row := range []*testTable{ {Name: "小李", Sex: "男", Counter: uint64(rand.Int63()), MissCounter: uint64(rand.Int63())}, {Name: "小白", Sex: "女", Counter: uint64(rand.Int63()), MissCounter: uint64(rand.Int63())}, {Name: "小黑", Sex: "男", Counter: uint64(rand.Int63()), MissCounter: uint64(rand.Int63())}, {Name: "小天", Sex: "女", Counter: uint64(rand.Int63()), MissCounter: uint64(rand.Int63())}, } { res, errExec := stmt.ExecContext(ctx, row) if errExec != nil { // for duplicate err if me, isMySQLErr := errExec.(*mysql.MySQLError); isMySQLErr && me.Number == 1062 /*duplicate is 1062*/ { log.Infof("skip duplicate err, err: %v, row: %+v", me, row) continue } log.Errorf("failed to stmt exec, err: %v", errExec) _ = tx.Rollback() return } insertID, _ := res.LastInsertId() log.Infof("success to stmt exec, row: %+v, id: %d", row, insertID) } if errCommit := tx.Commit(); errCommit != nil { log.Error(errCommit) return } log.Infof("finish") } ``` 4. 在使用全部小写的 NameMapper 情况下,程序执行会报错 `could not find name xxx`。 ```shell 2021-10-31T16:29:15.029+0800 ERROR transaction/main.go:76 failed to stmt exec, err: could not find name miss_counter in &main.testTable{ID:0x0, Name:"小李, Sex:"男", Counter:0x4354432fe0f37484, MissCounter:0x45afa115903b7669, CreateAt:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), UpdateAt:time.Date(1, tim.January, 1, 0, 0, 0, 0, time.UTC)} ``` 5. 在使用下划线命名法的 NameMapper 情况下,程序会顺利执行。 ```shell 2021-10-31T16:30:27.841+0800 INFO transaction/main.go:81 success to stmt exec, row: &{ID:0 Name:小李 Sex:男 Counter:6239523519032355576 MissCounter:180608923895024 CreateAt:0001-01-01 00:00:00 +0000 UTC UpdateAt:0001-01-01 00:00:00 +0000 UTC}, id: 23 2021-10-31T16:30:27.842+0800 INFO transaction/main.go:81 success to stmt exec, row: &{ID:0 Name:小白 Sex:女 Counter:2433768853952726858 MissCounter:7832533413053117 CreateAt:0001-01-01 00:00:00 +0000 UTC UpdateAt:0001-01-01 00:00:00 +0000 UTC}, id: 24 2021-10-31T16:30:27.844+0800 INFO transaction/main.go:81 success to stmt exec, row: &{ID:0 Name:小黑 Sex:男 Counter:8462818185278118914 MissCounter:1149211500697067 CreateAt:0001-01-01 00:00:00 +0000 UTC UpdateAt:0001-01-01 00:00:00 +0000 UTC}, id: 25 2021-10-31T16:30:27.845+0800 INFO transaction/main.go:81 success to stmt exec, row: &{ID:0 Name:小天 Sex:女 Counter:23243666993166910 MissCounter:9005766596808865 CreateAt:0001-01-01 00:00:00 +0000 UTC UpdateAt:0001-01-01 00:00:00 +0000 UTC}, id: 26 2021-10-31T16:30:27.848+0800 INFO transaction/main.go:87 finish ``` 如果本文对您有所帮助,可以给笔者点个赞,谢谢。

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

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

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