背景
使用ShardingSphere分表组件,遇到了这么一个报错。这个报错的意思是指,分表键的类型没有实现Comparble接口,导致了这个报错。
问题场景描述
以一个具体的查询为例,我的分表规则是基于 order_no
实现的,具体做法是截取 order_no
的前四位,即表名遵循 t_opay_${{order_no.substring(0, 4)}}
的规则。当执行如下查询时:
SELECT id,order_no,tid FROM `t_opay` WHERE order_no in ('20241118339590899049312411542')
在我的程序里,order_no
字段声明的是 string
类型,我使用 Golang 的 Gorm 来连接分表组件并进行数据库操作。然而,ShardingSphereProxy 实际接收到的 order_no
数据类型却是 []byte
,而 []byte
类型并未实现 Comparable
接口,这就导致了报错的出现。
数据库表结构
我们对t_opay
基于 order_no进行分表,共分为八张表,以年为维度,物理表从t_opay_2023到t_opay_2030。
以下是表结构的 SQL 代码:
-- ----------------------------
-- Table structure for t_opay_2022
-- ----------------------------
DROP TABLE IF EXISTS `t_opay_2022`;
CREATE TABLE `t_opay_2022` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '',
`prepay_id` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '微信统一下单返回(只有两小时的有效期)',
`tid` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '微信支付订单号',
`pay_type` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '支付方式(1:微信小程序;2:微信H5支付)',
`h5_url` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT 'H5调起支付页面路径',
`price` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '实付款',
`ptime` int UNSIGNED NOT NULL DEFAULT 0 COMMENT 'prepay_id最后生成时间',
`pay_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付时间',
`payment_plan` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'NO_INST' COMMENT '支付方案: NO_INST:不分期, INST_3:3期分期, INST_6:6期分期, INST_12:12期分期, FREE_3:3期免息, FREE_6:6期免息, FREE_12:12期免息',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `u_o`(`order_no` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14529 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单支付流水' ROW_FORMAT = COMPACT;
解决方案
经过不断地尝试和探索,我找到了一种有效的解决方案。那就是不要直接使用 Gorm 拼接查询条件,因为像下面这种方式会导致上述错误:
db.Where("order_no in ?", filter.OrderNos)
通过直接编写原生 SQL 的方式,可以成功解决这个问题,具体代码如下:
db.Where("order_no in ('?')", gorm.Expr(strings.Join(filter.OrderNos, "','")))
完整测试代码
package main
import (
"context"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var globalDB *gorm.DB
func InitDb() *gorm.DB {
log := logger.Default.LogMode(logger.Info)
// 连接到 MySQL 数据库
dsn := "root:root@tcp({你的连接信息})/sharding?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
}), &gorm.Config{
Logger: log,
})
if err != nil {
panic("failed to connect database")
}
globalDB = db
return db
}
func main() {
InitDb()
orderPayModelInstance := &OrderPayModel{}
ctx := context.Background()
filter := &OrderPayListFilter{
OrderNos: []string{"20241118339590899049312411542", "20250217533640010482822502144", "20250217533640010482822502145"},
}
list, _, err := orderPayModelInstance.List(ctx, filter, false, "*")
if err != nil {
panic(err)
}
if len(list) > 0 {
for _, v := range list {
println("order_no:", v.OrderNo)
}
}
// 使用channel,等待外部信号,结束程序
c := make(chan struct{})
<-c
}
// 数据表实体
type OrderPayEntity struct {
Id int `gorm:"column:id;type:int unsigned;primary_key;autoIncrement"` //
OrderNo string `gorm:"column:order_no;type:varchar(50)"` //
PrepayId string `gorm:"column:prepay_id;type:varchar(2000)"` //微信统一下单返回(只有两小时的有效期)/支付宝统一下单返回
Tid string `gorm:"column:tid;type:varchar(50)"` //微信支付订单号
PayType int `gorm:"column:pay_type;type:tinyint unsigned"` //支付方式(1:微信小程序;2:微信H5支付;3:微信APP支付;4:支付宝APP支付;6:先试后买)
H5Url string `gorm:"column:h5_url;type:varchar(500)"` //H5调起支付页面路径
Price int `gorm:"column:price;type:int unsigned"` //实付款
Ptime int64 `gorm:"column:ptime;type:int unsigned"` //prepay_id最后生成时间
PayTime int64 `gorm:"column:pay_time;type:int unsigned"` //支付时间
PaymentPlan string `gorm:"column:payment_plan;type:varchar(20)"` //支付方案: NO_INST:不分期, INST_3:3期分期, INST_6:6期分期, INST_12:12期分期, FREE_3:3期免息, FREE_6:6期免息, FREE_12:12期免息
}
// 函数方法归属体
type OrderPayModel struct {
}
// 表名
func OrderPayTableName() string {
return "t_opay"
}
// filter 查询条件
type OrderPayListFilter struct {
OrderNos []string `json:"order_nos" form:"order_nos"` //订单号列表
OrderBy string `json:"order_by" form:"order_by"` //排序规则
Page int `json:"page" form:"page"` //当前页码
PageSize int `json:"page_size" form:"page_size"` //每页数据条数
}
func (m OrderPayModel) List(ctx context.Context, filter *OrderPayListFilter, withTotal bool, fields string) ([]*OrderPayEntity, int64, error) {
var total int64
list := make([]*OrderPayEntity, 0)
db := globalDB.WithContext(ctx).Table(OrderPayTableName())
// todo buildQuery
m.buildQuery(db, filter)
//db.Where("order_no in ('20241118339590899049312411542')")
//db.Where(fmt.Sprintf("order_no in ('%s')", strings.Join(filter.OrderNos, "','")))
//db.Where("order_no in ('?')", gorm.Expr(strings.Join(filter.OrderNos, "','")))
// 是否查询总数,部分场景下不需要
if withTotal {
if err := db.Count(&total).Error; err != nil {
return list, 0, err
}
}
// 排序
if filter.OrderBy != "" {
db.Order(filter.OrderBy)
}
// 分页查询
if filter.Page != 0 && filter.PageSize != 0 {
page := filter.Page
pageSize := filter.PageSize
db = db.Limit(pageSize).Offset((page - 1) * pageSize)
}
// 查询数据
err := db.Select(fields).Find(&list).Error
if err != nil {
return list, total, err
}
return list, total, err
}
func (m OrderPayModel) buildQuery(db *gorm.DB, filter *OrderPayListFilter) {
//增加一个判断,避免没有查询条件时全表查询
var isEmpty bool = true
if len(filter.OrderNos) > 0 {
isEmpty = false
//db.Where("order_no in ?", filter.OrderNos)
db.Where("order_no in ('?')", gorm.Expr(strings.Join(filter.OrderNos, "','")))
}
// 没有查询条件时,不执行查询操作,避免全表扫描
if isEmpty {
db.Where("order_no = ?", "-1")
}
}
待解决的问题
尽管找到了临时的解决方案,但我仍然对使用 Gorm 导致数据类型变化的原因感到困惑。我仔细查看了 Gorm 的源码,却未能找到产生这个问题的逻辑。我猜测这可能与驱动有关,但目前还不能确定。后续有时间,我会继续深入研究,探寻这背后的真正原因。
如果有哪位大牛知道这个背后的原因,帮忙指点一二,不胜感激。