业务背景 有这样的业务场景,线上一个表tablea,线上环境还有一个镜像表tableamirror,你需要当请求中有一些tag标识的时候,访问tableamirror表先安装sqlite https:wangxiaoming。blog。csdn。netarticledetails121884736代码 可以使用DefaultTableNameHandler来实现加前缀或者后缀功能。import(code。byted。orggopkggormcontext)typedbStagingPostfixKeyTypestruct{}vardbStagingPostfixKeydbStagingPostfixKeyType{}funcWithDbStagingPostfix(ctxcontext。Context,postfixstring)context。Context{returncontext。WithValue(ctx,dbStagingPostfixKey,postfix)}funcReWriteTableName(){gorm。DefaultTableNameHandlerfunc(dbgorm。DB,defaultTableNamestring)string{ifv:db。Ctx。Value(dbStagingPostfixKey);v!nil{returndefaultTableNamev。(string)}returndefaultTableName}}测试代码packagemysqlimport(fmttestinggithub。comjinzhugormgithub。comjinzhugormdialectssqlitegithub。commattngosqlite3)typeProductstruct{gorm。ModelCodestringPriceuint}func(Product)TableName()string{returnhaxproducts}funcTest(ttesting。T){db,err:gorm。Open(sqlite3,test。db)iferr!nil{panic(failedtoconnectdatabase)}deferdb。Close()gorm。DefaultTableNameHandlerfunc(dbgorm。DB,defaultTableNamestring)string{returnhaxdefaultTableName}db。LogMode(true)Migratetheschemadb。AutoMigrate(Product{})db。Create(Product{Code:L1212,Price:1000})varproductProductdb。First(product,1)varproducts〔〕Productdb。Find(products)fmt。Printf(Totalcountd,len(products))} 执行结果:(Usersxxxgosrcxxxxxx。xxGoProjectmysqlsqllitetest。go:33)〔2021121421:43:33〕〔1。38ms〕INSERTINTOhaxproducts(createdat,updatedat,deletedat,code,price)VALUES(2021121421:43:33,2021121421:43:33,NULL,L1212,1000)〔1rowsaffectedorreturned〕(Usersxxxgosrcxxxxxx。xxGoProjectmysqlsqllitetest。go:35)〔2021121421:43:33〕〔0。23ms〕SELECTFROMhaxproductsWHEREhaxproducts。deletedatISNULLAND((haxproducts。id1))ORDERBYhaxproducts。idASCLIMIT1〔1rowsaffectedorreturned〕((Usersxxxgosrcxxxxxx。xxGoProjectmysqlsqllitetest。go:37)〔2021121421:43:33〕nosuchtable:haxhaxproducts((Usersxxxgosrcxxxxxx。xxGoProjectmysqlsqllitetest。go:37)〔2021121421:43:33〕〔0。10ms〕SELECTFROMhaxhaxproductsWHEREhaxhaxproducts。deletedatISNULL〔0rowsaffectedorreturned〕Totalcount0PASS:Test(0。00s)PASS 根据执行结果,可以看到,创建语言与查询单条记录时表名为haxproducts但是查询多条记录时,却使用了表名haxhaxproducts。这个就是坑1 查询单个记录时使用了TableName()返回的表名,而在查询结果为Array时,表名在TableName()的基础上又添加了前缀。 Gorm结构体一般分析如下structtypeDBstruct(gormmain。go)代表数据库连接,每次操作数据库会创建出clone对象。方法gorm。Open()返回的值类型就是这个结构体指针。typeScopestruct(gormscope。go)当前数据库操作的信息,每次添加条件时也会创建clone对象。typeCallbackstruct(gormcallback。go)数据库各种操作的回调函数,SQL生成也是靠这些回调函数。每种类型的回调函数放在单独的文件里,比如查询回调函数在gormcallbackquery。go,创建的在gormcallbackcreate。godb。First()代码分析 First()方法位于gormmain。go文件中,。callCallbacks(s。parent。callbacks。queries)调用了query回调函数。file:gormmain。goFirstfindfirstrecordthatmatchgivenconditions,orderbyprimarykeyfunc(sDB)First(outinterface{},where。。。interface{})DB{newScope:s。NewScope(out)newScope。Search。Limit(1)returnnewScope。Set(gorm:orderbyprimarykey,ASC)。inlineCondition(where。。。)。callCallbacks(s。parent。callbacks。queries)。db} Callback结构体中定义queries为函数指针数组,而默认值的初始化在gormcallbackquery。go的init()方法中,查询方法为queryCallback,而queryCallback()方法又调用到scope。prepareQuerySQL(),scope中的方法真正生成SQL的地方。file:gormcallback。gotypeCallbackstruct{loggerloggercreates〔〕func(scopeScope)updates〔〕func(scopeScope)deletes〔〕func(scopeScope)queries〔〕func(scopeScope)rowQueries〔〕func(scopeScope)processors〔〕CallbackProcessor}file:gormcallbackquery。goDefinecallbacksforqueryingfuncinit(){DefaultCallback。Query()。Register(gorm:query,queryCallback)DefaultCallback。Query()。Register(gorm:preload,preloadCallback)DefaultCallback。Query()。Register(gorm:afterquery,afterQueryCallback)}queryCallbackusedtoquerydatafromdatabasefuncqueryCallback(scopeScope){。。。scope。prepareQuerySQL()。。。} 跟踪代码到scope。go文件,函数TableName()是获取数据库表名的地方。它按如下顺序来确定表名:scope。Search。tableName查询条件中设置了表名,则直接使用scope。Value。(tabler)值对象实现了tabler接口(方法TableName()string),则从调用方法获取scope。Value。(dbTabler)值对象实现了dbTabler接口(方法TableName(DB)string),则从调用方法获取若以上条件都不成立,则从scope。GetModelStruct()中获取对象的结构体信息,从结构体名生成表名 具体可见scope。go源码file:gormscope。gofunc(scopeScope)prepareQuerySQL(){ifscope。Search。raw{scope。Raw(scope。CombinedConditionSql())}else{scope。Raw(fmt。Sprintf(SELECTvFROMvv,scope。selectSQL(),scope。QuotedTableName(),scope。CombinedConditionSql()))}return}QuotedTableNamereturnquotedtablenamefunc(scopeScope)QuotedTableName()(namestring){ifscope。Search!nillen(scope。Search。tableName)0{ifstrings。Contains(scope。Search。tableName,){returnscope。Search。tableName}returnscope。Quote(scope。Search。tableName)}returnscope。Quote(scope。TableName())}TableNamereturntablenamefunc(scopeScope)TableName()string{ifscope。Search!nillen(scope。Search。tableName)0{returnscope。Search。tableName}iftabler,ok:scope。Value。(tabler);ok{returntabler。TableName()}iftabler,ok:scope。Value。(dbTabler);ok{returntabler。TableName(scope。db)}returnscope。GetModelStruct()。TableName(scope。db。Model(scope。Value))} 对比以上条件,示例中的Product结构体定义了方法TableName()string,符合条件2,那么db。First(product,1)使用的表名就是haxproducts。db。Find()代码分析 Find()代码如下,与First()同样是使用了callbacks。queries回调方法,不同点在于设置了newScope。Search。Limit(1)只返回一个结果、增加了按id排序。Findfindrecordsthatmatchgivenconditionsfunc(sDB)Find(outinterface{},where。。。interface{})DB{returns。NewScope(out)。inlineCondition(where。。。)。callCallbacks(s。parent。callbacks。queries)。db} 在debug模式下跟踪代码到scope。TableName()中时,两次查询的区别显示出来了:它们的结果值类型不同。db。First(product,1)的值类型为结构体的指针Product,而db。Find(products)的值类型是数组的指针〔〕Product,从而导致db。Find(products)进入条件scope。GetModelStruct()。TableName(scope。db。Model(scope。Value))},需要靠分析struct结构体来生成表名。file:gormmodelstruct。goTableNamereturnsmodelstablenamefunc(sModelStruct)TableName(dbDB)string{s。l。Lock()defers。l。Unlock()ifs。defaultTableNamedb!nils。ModelType!nil{Setdefaulttablenameiftabler,ok:reflect。New(s。ModelType)。Interface()。(tabler);ok{s。defaultTableNametabler。TableName()}else{tableName:ToTableName(s。ModelType。Name())db。parent。RLock()ifdbnil(db。parent!nil!db。parent。singularTable){tableNameinflection。Plural(tableName)}db。parent。RUnlock()s。defaultTableNametableName}}returnDefaultTableNameHandler(db,s。defaultTableName)} 默认表名s。defaultTableName为空值时先进行求值,reflect。New(s。ModelType)。Interface()。(tabler)先判断是否实现了tabler接口,有则调用其TableName()取值;否则的话从结构体的名字来生成表名。结果返回之前再调用DefaultTableNameHandler(db,s。defaultTableName)方法。 这个ModelStruct的TableName方法与scope。TableName()中的逻辑两个不一致的地方:scope。TableName()会判断是否实现tabler与dbTabler两个接口,而这里只判断了tablerscope。TableName()是将tableName结果直接返回的,而这里多调用了DefaultTableNameHandler()。 因为逻辑scope。TableName()的存在,当重写DefaultTableNameHandler()方法时,就会出现表前缀再次被添加了表名前。问题2 DefaultTableNameHandler()在多数据库时出现混乱 通过以上代码的分析,于是发现了另一个坑:当一个程序中使用两个不同的数据库时,重写方法DefaultTableNameHandler()会影响到两个数据库中的表名。其中一个数据库需要设置表前缀时,访问另一个数据库的表也可能会被加上前缀。因为是包级别的方法,整个代码里只能设置一次值。file:gormmodelstruct。goDefaultTableNameHandlerdefaulttablenamehandlervarDefaultTableNameHandlerfunc(dbDB,defaultTableNamestring)string{returndefaultTableName}总结当给结构体实现了TableName()方法时,就不要设置DefaultTableNameHandler了。保持所有Model的表名生成方式一致,要么全部使用自动生成的表名,要么全部实现tabler接口(实现TableName()方法)当需要使用多个数据库时,要避免设置DefaultTableNameHandler强烈建议:所有Model结构体全部实现tabler接口欢迎关注:程序员财富自由之路 在这里插入图片描述参考资料http:blog。vikazhou。com20200209GORMProblemAnalyze