feat(gdb): gdb.Model.Scan supports the IAfterScan interface for passed-in objects.#4624
feat(gdb): gdb.Model.Scan supports the IAfterScan interface for passed-in objects.#4624smzgl wants to merge 14 commits intogogf:masterfrom
Conversation
|
@smzgl 这个想法挺不错的,不过请完整描述下业务场景呢以便大家评估这个特性的必要性。此外,CI失败了,源码中注释需要统一使用英文,建议使用AI改进一下。 |
There was a problem hiding this comment.
Pull request overview
This pull request adds support for the IAfterScan interface in gdb.Model.Scan, enabling custom post-scan operations on database-scanned objects. This allows developers to execute common logic automatically after struct fields have been populated with database data.
Changes:
- Added new
IAfterScaninterface that structs can implement to define post-scan behavior - Modified
doWithScanStructto callAfterScanon single struct scans - Modified
doWithScanStructsto callAfterScanon each element in struct slice scans - Added
doAfterScanhelper method to handle reflection logic and interface checking
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // IAfterScan is an interface that can be implemented by structs to perform | ||
| // custom logic after the struct has been populated with data from the database. | ||
| // The AfterScan method is called after the struct fields have been filled with | ||
| // data retrieved from the database during scanning operations. | ||
| type IAfterScan interface { | ||
| AfterScan() error | ||
| } |
There was a problem hiding this comment.
The new IAfterScan interface functionality lacks test coverage. Consider adding tests to verify: 1) AfterScan is called correctly for single structs, 2) AfterScan is called for each element in struct slices, 3) AfterScan handles multi-level pointers correctly, 4) AfterScan errors are properly propagated, and 5) structs that don't implement IAfterScan continue to work normally.
| // 遍历切片中的每个元素 | ||
| for i := 0; i < arrayValue.Len(); i++ { | ||
| element := arrayValue.Index(i) | ||
| if err = m.doAfterScan(element.Interface()); err != nil { |
There was a problem hiding this comment.
When iterating over slice elements, element.Interface() returns the actual element value. If the slice contains struct values (not pointers like []User), the AfterScan method won't be called even if the struct has a pointer receiver implementation of AfterScan. Consider using element.Addr().Interface() to get the address of the element, ensuring AfterScan is called for both []User and []*User slice types.
| if err = m.doAfterScan(element.Interface()); err != nil { | |
| var target any | |
| if element.Kind() == reflect.Ptr { | |
| // 切片元素本身就是指针类型,直接使用 | |
| target = element.Interface() | |
| } else if element.CanAddr() { | |
| // 切片元素为值类型,取其地址以便触发指针接收者的 AfterScan | |
| target = element.Addr().Interface() | |
| } else { | |
| // 非可寻址值作为兜底处理(理论上切片元素应是可寻址的) | |
| target = element.Interface() | |
| } | |
| if err = m.doAfterScan(target); err != nil { |
There was a problem hiding this comment.
我理解, 如果前面的代码能正常完成, 基本到了这步应该不需要做这个判断.
| // return nil | ||
| // } | ||
|
|
||
| // 找到最终的指针(处理多级指针) |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "Find the final pointer (handling multi-level pointers)"
| // 找到最终的指针(处理多级指针) | |
| // Find the final pointer (handling multi-level pointers) |
|
|
||
| // 找到最终的指针(处理多级指针) | ||
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | ||
| // 如果当前指针指向的还是指针,继续深入 |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "If the current pointer still points to a pointer, continue drilling down"
| // 如果当前指针指向的还是指针,继续深入 | |
| // If the current pointer still points to a pointer, continue drilling down |
| // 处理多级指针的情况,找到最终的指针用于接口检查 | ||
| var ptrValue reflect.Value | ||
|
|
||
| switch v := pointer.(type) { | ||
| case reflect.Value: | ||
| // 已经是 reflect.Value | ||
| ptrValue = v | ||
| default: | ||
| // 转换为 reflect.Value | ||
| ptrValue = reflect.ValueOf(pointer) | ||
| } | ||
|
|
||
| // 如果是 nil,直接返回 | ||
| // if ptrValue.IsNil() { | ||
| // return nil | ||
| // } | ||
|
|
||
| // 找到最终的指针(处理多级指针) | ||
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | ||
| // 如果当前指针指向的还是指针,继续深入 | ||
| if ptrValue.Elem().Kind() == reflect.Ptr { | ||
| ptrValue = ptrValue.Elem() | ||
| } else { | ||
| // 找到了最终的指针(指向非指针类型) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| // 确保 ptrValue 是指针类型且非空 | ||
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | ||
| return nil | ||
| } | ||
|
|
||
| // 检查指针是否实现了 IAfterScan 接口 | ||
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | ||
| // 调用 AfterScan 方法 |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency with the rest of the codebase, this comment should be in English. It should read something like: "Handle multi-level pointers to find the final pointer for interface checking"
| // 处理多级指针的情况,找到最终的指针用于接口检查 | |
| var ptrValue reflect.Value | |
| switch v := pointer.(type) { | |
| case reflect.Value: | |
| // 已经是 reflect.Value | |
| ptrValue = v | |
| default: | |
| // 转换为 reflect.Value | |
| ptrValue = reflect.ValueOf(pointer) | |
| } | |
| // 如果是 nil,直接返回 | |
| // if ptrValue.IsNil() { | |
| // return nil | |
| // } | |
| // 找到最终的指针(处理多级指针) | |
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | |
| // 如果当前指针指向的还是指针,继续深入 | |
| if ptrValue.Elem().Kind() == reflect.Ptr { | |
| ptrValue = ptrValue.Elem() | |
| } else { | |
| // 找到了最终的指针(指向非指针类型) | |
| break | |
| } | |
| } | |
| // 确保 ptrValue 是指针类型且非空 | |
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | |
| return nil | |
| } | |
| // 检查指针是否实现了 IAfterScan 接口 | |
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | |
| // 调用 AfterScan 方法 | |
| // Handle multi-level pointers and find the final pointer for interface checking. | |
| var ptrValue reflect.Value | |
| switch v := pointer.(type) { | |
| case reflect.Value: | |
| // Pointer is already a reflect.Value. | |
| ptrValue = v | |
| default: | |
| // Convert pointer to reflect.Value. | |
| ptrValue = reflect.ValueOf(pointer) | |
| } | |
| // Early return for nil values (if enabled). | |
| // if ptrValue.IsNil() { | |
| // return nil | |
| // } | |
| // Find the final pointer by resolving multi-level pointers. | |
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | |
| // If the current pointer points to another pointer, keep dereferencing. | |
| if ptrValue.Elem().Kind() == reflect.Ptr { | |
| ptrValue = ptrValue.Elem() | |
| } else { | |
| // Reached the final pointer that points to a non-pointer type. | |
| break | |
| } | |
| } | |
| // Ensure ptrValue is a non-nil pointer type. | |
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | |
| return nil | |
| } | |
| // Check whether the pointer implements the IAfterScan interface. | |
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | |
| // Invoke the AfterScan method. |
| // 处理多级指针的情况,找到最终的指针用于接口检查 | ||
| var ptrValue reflect.Value | ||
|
|
||
| switch v := pointer.(type) { | ||
| case reflect.Value: | ||
| // 已经是 reflect.Value | ||
| ptrValue = v | ||
| default: | ||
| // 转换为 reflect.Value | ||
| ptrValue = reflect.ValueOf(pointer) | ||
| } | ||
|
|
||
| // 如果是 nil,直接返回 | ||
| // if ptrValue.IsNil() { | ||
| // return nil | ||
| // } | ||
|
|
||
| // 找到最终的指针(处理多级指针) | ||
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | ||
| // 如果当前指针指向的还是指针,继续深入 | ||
| if ptrValue.Elem().Kind() == reflect.Ptr { | ||
| ptrValue = ptrValue.Elem() | ||
| } else { | ||
| // 找到了最终的指针(指向非指针类型) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| // 确保 ptrValue 是指针类型且非空 | ||
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | ||
| return nil | ||
| } | ||
|
|
||
| // 检查指针是否实现了 IAfterScan 接口 | ||
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | ||
| // 调用 AfterScan 方法 |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "Convert to reflect.Value"
| // 处理多级指针的情况,找到最终的指针用于接口检查 | |
| var ptrValue reflect.Value | |
| switch v := pointer.(type) { | |
| case reflect.Value: | |
| // 已经是 reflect.Value | |
| ptrValue = v | |
| default: | |
| // 转换为 reflect.Value | |
| ptrValue = reflect.ValueOf(pointer) | |
| } | |
| // 如果是 nil,直接返回 | |
| // if ptrValue.IsNil() { | |
| // return nil | |
| // } | |
| // 找到最终的指针(处理多级指针) | |
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | |
| // 如果当前指针指向的还是指针,继续深入 | |
| if ptrValue.Elem().Kind() == reflect.Ptr { | |
| ptrValue = ptrValue.Elem() | |
| } else { | |
| // 找到了最终的指针(指向非指针类型) | |
| break | |
| } | |
| } | |
| // 确保 ptrValue 是指针类型且非空 | |
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | |
| return nil | |
| } | |
| // 检查指针是否实现了 IAfterScan 接口 | |
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | |
| // 调用 AfterScan 方法 | |
| // Handle multi-level pointers and find the final pointer for interface checking. | |
| var ptrValue reflect.Value | |
| switch v := pointer.(type) { | |
| case reflect.Value: | |
| // Already a reflect.Value. | |
| ptrValue = v | |
| default: | |
| // Convert to reflect.Value. | |
| ptrValue = reflect.ValueOf(pointer) | |
| } | |
| // If it is nil, return directly. | |
| // if ptrValue.IsNil() { | |
| // return nil | |
| // } | |
| // Find the final pointer (handle multi-level pointers). | |
| for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() { | |
| // If the current pointer still points to a pointer, go deeper. | |
| if ptrValue.Elem().Kind() == reflect.Ptr { | |
| ptrValue = ptrValue.Elem() | |
| } else { | |
| // Found the final pointer (pointing to a non-pointer type). | |
| break | |
| } | |
| } | |
| // Ensure ptrValue is a non-nil pointer type. | |
| if (ptrValue.Kind() != reflect.Ptr) || ptrValue.IsNil() { | |
| return nil | |
| } | |
| // Check whether the pointer implements the IAfterScan interface. | |
| if afterScanner, ok := ptrValue.Interface().(IAfterScan); ok { | |
| // Call AfterScan method. |
| // 如果是 nil,直接返回 | ||
| // if ptrValue.IsNil() { | ||
| // return nil | ||
| // } | ||
|
|
There was a problem hiding this comment.
This commented-out code should be removed. Dead code reduces maintainability and can be confusing. If nil checking is not needed, remove this entirely. If it might be needed in the future, it can be retrieved from version control.
| // 如果是 nil,直接返回 | |
| // if ptrValue.IsNil() { | |
| // return nil | |
| // } |
| } | ||
| } | ||
|
|
||
| // 确保 ptrValue 是指针类型且非空 |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "Ensure ptrValue is a pointer type and non-nil"
| // 确保 ptrValue 是指针类型且非空 | |
| // Ensure ptrValue is a pointer type and non-nil |
| } | ||
|
|
||
| arrayValue := reflect.ValueOf(pointer) | ||
| // 找到最终的指针(处理多级指针) |
There was a problem hiding this comment.
The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "Find the final pointer (handling multi-level pointers)"
|
可以接受copilot给的翻译建议 |
|
能描述下这个是为了解决什么实际问题或者需求吗?我不太理解为什么要把一些通用操作让gdb去执行而不是在外部自己调用,是为了类似hook的功能吗? |
我改了好几次, 就是不知道哪里有问题导致CI没过. 请教一下, 仅仅是注释问题吗? 我看提示错误行是在代码上. |
举个常见的例子: 有2个表, 分别是用户表和业务表 create table user {
id bigint not null auto_increment comment '自增ID',
name varchar(50) not null comment '姓名',
}
create table biz {
id bigint not null auto_increment comment '自增ID',
name varchar(50) not null comment '业务名称',
creator_id bigint comment '创建人ID'
}实现获取 biz 列表的接口, 要求返回 业务数据, 以及创建人id和姓名. 现在的做法, 一般有两种:
type BizItem struct {
*entity.BizItem
CreatorName string `orm:"creator_name"`
}如果结构复杂, 比如涉及到好几个表联合, LeftJoin 会很丑陋, 特别是里面涉及到字段名称的 都不写死, 而用 dao.Biz.Columns() 进行字段拼接的情况下.
////////////////////////分割线///////////////////////////////////// 如果有IAfterScan的情况下. 就可以充分利用goframe现有WithAll()的特性来帮忙我们完成这件事. type BizItem struct {
*entity.BizItem
User *entity.User `json:"-" orm:"with:id=creator_id"
CreatorName string `json:"creator_name"
}
func (item *BizItem) AfterScan() error {
if item.User != nil {
item.CreatorName = item.User.Name
}
return nil
}这样, 我们在读取数据的时候, 代码就可以简化为一行代码. var item *BizItem
dao.Biz.Ctx(ctx).WithAll().WherePri(xxx).Scan(&item)同时在API定义中, 可以直接使用该结构体, 而不需要重新定义一个并赋值. type GetBizList struct {
g.Meta `path:"/biz/list" method:"get"`
}
type GetBizListRes struct {
g.Meta `mime:"application/json"`
Total int64 `json:"total"
List []*BizItem `json:"list"`
}当Biz关联了好几个表, 在获取Biz信息时候, 也需要实时获取关联表的名称的时候, 这样的写法可以极大提升编码效率(少写了不少代码), 保持代码简洁. 同时如果今后Biz表新增了字段, 支持要运行gf gen dao 重新生成dao和entity对象, 相关接口新增字段就会自动增加. 至于为什么不用LeftJoin, 一方面目前系统没有那么大的并发以及数量, 并且在接口延迟上, 能接受因为多了几次的数据库访问导致的延时增加. 而开发效率和保持代码简洁, 是目前比较在意的. 那为什么不在Controller做赋值转换呢, 正如第一点提到的, 希望能直接复用entity的结构体的同时, 又能同时满足接口返回结构的要求. 如果类似的接口和逻辑有多处, 则还需要再一次封装才行, 这无疑增加了代码量. |
Annotate in English Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
想法不错,但是可以做更好,比如直接hook Scan逻辑的前后,不局限于Scan之后,使用者可以自定义什么时机调用, // /gdb/xxx.go
type IModelScan Interface{
Scan(any)error
}
// pointer是原Scan方法的参数
// pointer参数似乎也可以直接用item代替???看实现吧
// ms参数似乎可以直接用*gdb.Model,让使用者自定义一些其他方法的逻辑也可以,不局限于Scan???
func(item *BizItem) HookScan(pointer any,ms gdb.IModelScan )error {
// before scan
err:=ms.Scan(pointer) // 原Scan逻辑
// after scan
return err
} |
|
我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32 |
|
好像没看到你代码中如合gdb使用的例子。 而且你的方式,可能和我的需求稍微有点偏差。 你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。
|
|
每个人都会有自己的想法,我也只是抛出我的想法,剩下的可以看看其他小伙伴有没有更不一样的方案。 |
明白你的意思了, 这的确也是一个不错的思路. |
不能认同你的gexecutor方案来套用此思路,你所说的 【 |
…hin the function to be within the same transaction as the caller.
|
大佬们,求关注,亲还问,能有结论吗? |
|
是否可以先放到自定义驱动里,在使用上当作实验功能,先运行一段时间。 |
以下仅是我个人不成熟看法以及对框架的理解, 说得不对的地方希望不要介意。 我认为作为框架其中一个重要作用, 就是框定架构, 让各种业务有章可循, 同时提供标准的数据流和基础功能, 从而减少很多不必要的重复代码. 在这个过程中, 通过接口、钩子、约定、注解、切面等各种方式把各个环节的暴露出来,以满足各种各种的业务场景。 对比grpc, gin等框架等, 总能在各个数据流转节点找到相关接口, 来切入自己的业务逻辑, 无论有些逻辑个人看来多么不合理, 但是对于特定场景下却又是当下最合适的选择. 回到我的场景, 想要解决的问题: goframe实现的ORM中,对表关联支持不是那么完善,特别是不希望硬编码表名称和表字段,以及不希望自行再重复声明一个跟entity差不多的结构体的时候。当有多级1对多表关联的时候, 我需要给前端返回一个干净的结构体. controller中各种循环赋值仅仅是为了给输出整形. 这是非常繁琐并且容易出错的, 特别是在业务多变的情况下. 而且这个接口恰好能解决我的问题. 你提到的业务上能处理的场景就在业务处理. "业务" 本身就是如何看待的问题, 我个人认为哪些属于业务哪些属于组件, 很多时候并不是那么的绝对. 每个业务总会把自己认为繁琐的, 可以复用的逻辑再次封装. 不断沉淀后, 何尝不是一个新组件呢? |
这个场景的写法感觉是一个变体的 真的优化的话我觉可能还是 type BizItem struct {
*entity.BizItem
CreatorName string `json:"creator_name" orm:"table:users with:id=creator_id field:name"`
}这样的效果的话,丑是丑了点,但是你的场景能够得到很好的解决。 @smzgl @wln32 @LanceAdd @gqcn 不知道v3有没有新的 |
在MarshalJSON中, 实现结构体整型是一种方法. 但是这会有一些局限性: 题外话: 现在的表关联的确太弱, 光有with很多场景是无法满足的. 而用Join的情况下进行Scan, 不得不自己写一个包含两个表字段的结构体(这意味着字段名要硬编码), 而且现在的Join.Scan 在字段同名的情况下, 是有问题的, 除非使用as. 当两个宽表join的时候, 这是一个灾难. 我目前的一个解决方案是, 针对mariadb, 连接数据库的时候增加columnsWithAlias=true, select的时候会返回表名. 同时 gf gen dao 命令增加参数, 使得生成的entity字段也带上表名. 这样当使用Scan的时候就可以正确为下面结构体赋值. type UserListItem struct { 配置IAfterScan, 可以实现对表名和字段名零硬编码的情况下, 实现表关联后的结构体进行数据整形. 针对我目前需求多变, 设计多变, 表名字段名多变的情况下是比较省心和安全的. 毕竟只要重新执行gf gen dao, 任何数据库变动相关变动编辑器直接报错, 然后进行修改. 关于框架对外暴露切面切入点以及类似自定义序列化包这种需求社区内部讨论过很多次,需要调整很多东西,包括包的层级关系上浮下沉这种以及一系列调整,所以想要平滑兼容过度到V3基本不可能,所以才会有不向下兼容的V3计划,但是V2还是得继续先迭代开发修bug,最近因为大家都在捣鼓ai所以进度有点停滞等忙完这段时间就会处理积累的pr |
With/WithAll的优化我已经提交了一个 #4672 (等我这段时间忙完了再优化下)。然后针对于这种前后增强处理的需求我个人还是推荐在数据库查询外包一层,就像我之前回答里上传上来的例子 #4654 (纯为了这个issuse提交的,不会合并到框架里),当时我的具体回复可以往上翻一下,这种可复用的前后增强大家都能写出来。 |
feat(gdb): gdb.Model.Scan 对传入对象支持IAfterScan接口. 使得完成Scan后, 能执行通用的操作.