Skip to content

feat(gdb): gdb.Model.Scan supports the IAfterScan interface for passed-in objects.#4624

Open
smzgl wants to merge 14 commits intogogf:masterfrom
smzgl:feat-gdb-support-afterScan-interface
Open

feat(gdb): gdb.Model.Scan supports the IAfterScan interface for passed-in objects.#4624
smzgl wants to merge 14 commits intogogf:masterfrom
smzgl:feat-gdb-support-afterScan-interface

Conversation

@smzgl
Copy link
Copy Markdown
Contributor

@smzgl smzgl commented Jan 16, 2026

feat(gdb): gdb.Model.Scan 对传入对象支持IAfterScan接口. 使得完成Scan后, 能执行通用的操作.

@gqcn
Copy link
Copy Markdown
Member

gqcn commented Jan 21, 2026

@smzgl 这个想法挺不错的,不过请完整描述下业务场景呢以便大家评估这个特性的必要性。此外,CI失败了,源码中注释需要统一使用英文,建议使用AI改进一下。

@gqcn gqcn requested a review from Copilot January 21, 2026 11:30
@gqcn gqcn added feature discuss We need discuss to make decision. labels Jan 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IAfterScan interface that structs can implement to define post-scan behavior
  • Modified doWithScanStruct to call AfterScan on single struct scans
  • Modified doWithScanStructs to call AfterScan on each element in struct slice scans
  • Added doAfterScan helper method to handle reflection logic and interface checking

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +27
// 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
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// 遍历切片中的每个元素
for i := 0; i < arrayValue.Len(); i++ {
element := arrayValue.Index(i)
if err = m.doAfterScan(element.Interface()); err != nil {
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我理解, 如果前面的代码能正常完成, 基本到了这步应该不需要做这个判断.

Comment thread database/gdb/gdb_model_with.go Outdated
Comment thread database/gdb/gdb_model_with.go Outdated
// return nil
// }

// 找到最终的指针(处理多级指针)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)"

Suggested change
// 找到最终的指针(处理多级指针)
// Find the final pointer (handling multi-level pointers)

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated

// 找到最终的指针(处理多级指针)
for ptrValue.Kind() == reflect.Ptr && !ptrValue.IsNil() {
// 如果当前指针指向的还是指针,继续深入
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Suggested change
// 如果当前指针指向的还是指针,继续深入
// If the current pointer still points to a pointer, continue drilling down

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated
Comment on lines +349 to +384
// 处理多级指针的情况,找到最终的指针用于接口检查
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 方法
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Suggested change
// 处理多级指针的情况,找到最终的指针用于接口检查
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.

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated
Comment on lines +349 to +384
// 处理多级指针的情况,找到最终的指针用于接口检查
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 方法
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is written in Chinese while the codebase uses English. For consistency, this should be translated to: "Convert to reflect.Value"

Suggested change
// 处理多级指针的情况,找到最终的指针用于接口检查
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.

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated
Comment on lines +361 to +365
// 如果是 nil,直接返回
// if ptrValue.IsNil() {
// return nil
// }

Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 如果是 nil,直接返回
// if ptrValue.IsNil() {
// return nil
// }

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated
}
}

// 确保 ptrValue 是指针类型且非空
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"

Suggested change
// 确保 ptrValue 是指针类型且非空
// Ensure ptrValue is a pointer type and non-nil

Copilot uses AI. Check for mistakes.
Comment thread database/gdb/gdb_model_with.go Outdated
}

arrayValue := reflect.ValueOf(pointer)
// 找到最终的指针(处理多级指针)
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@houseme
Copy link
Copy Markdown
Member

houseme commented Jan 21, 2026

可以接受copilot给的翻译建议

@LanceAdd
Copy link
Copy Markdown
Member

能描述下这个是为了解决什么实际问题或者需求吗?我不太理解为什么要把一些通用操作让gdb去执行而不是在外部自己调用,是为了类似hook的功能吗?

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Jan 22, 2026

@smzgl 这个想法挺不错的,不过请完整描述下业务场景呢以便大家评估这个特性的必要性。此外,CI失败了,源码中注释需要统一使用英文,建议使用AI改进一下。

我改了好几次, 就是不知道哪里有问题导致CI没过. 请教一下, 仅仅是注释问题吗? 我看提示错误行是在代码上.

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Jan 22, 2026

能描述下这个是为了解决什么实际问题或者需求吗?我不太理解为什么要把一些通用操作让gdb去执行而不是在外部自己调用,是为了类似hook的功能吗?

举个常见的例子:

有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和姓名.

现在的做法, 一般有两种:

  1. 写LeftJoin, 自己构建一个 带有CreatorName字段的结构体,
type BizItem struct {
    *entity.BizItem 
   CreatorName string `orm:"creator_name"`
}

如果结构复杂, 比如涉及到好几个表联合, LeftJoin 会很丑陋, 特别是里面涉及到字段名称的 都不写死, 而用 dao.Biz.Columns() 进行字段拼接的情况下.

  1. 第二种, 是分别获取到数据后, 自行for循环遍历赋值.

////////////////////////分割线/////////////////////////////////////

如果有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的结构体的同时, 又能同时满足接口返回结构的要求. 如果类似的接口和逻辑有多处, 则还需要再一次封装才行, 这无疑增加了代码量.

smzgl and others added 2 commits January 22, 2026 13:11
Annotate in English

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@smzgl smzgl changed the title feat(gdb): gdb.Model.Scan 对传入对象支持IAfterScan接口 feat(gdb): gdb.Model.Scan supports the IAfterScan interface for passed-in objects. Jan 22, 2026
@wln32
Copy link
Copy Markdown
Member

wln32 commented Jan 22, 2026

想法不错,但是可以做更好,比如直接hook Scan逻辑的前后,不局限于Scan之后,使用者可以自定义什么时机调用,
@LanceAdd @gqcn 我比较赞同 @smzgl 的想法,这样只是扩展原有逻辑,并不对原来逻辑做修改

// /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
}

@LanceAdd
Copy link
Copy Markdown
Member

LanceAdd commented Jan 22, 2026

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Jan 23, 2026

好像没看到你代码中如合gdb使用的例子。

而且你的方式,可能和我的需求稍微有点偏差。

你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

@LanceAdd
Copy link
Copy Markdown
Member

好像没看到你代码中如合gdb使用的例子。

而且你的方式,可能和我的需求稍微有点偏差。

你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

AfterScan 是为 特定结构体 绑定 固定行为;而 gexecutor 是为 任意类型 提供 可组合的执行流程。
gexecutor是个泛型执行器,就如同你为指定结构体实现AfterScan方法一样,gexecutor也只需要为指定类型创建执行器,然后自己定义好beforeafter,剩下的你是想用gdb查询出这个还是自己直接new出来一个都随便,只需要withmain去定义一下就行。
你觉得不符合你需求是因为我举得例子都是用的int/struct之类的非指针类型,所以是不侵入的,如果需要before改变input输入以及after改变result输出,只需要输入输出都是指针类型就行了。这样你的BizItem 结构体本身不需要实现任何接口,保持纯净,你依然可以用 dao.Biz.Scan(&item),只是把 AfterScan 的逻辑挪到了 WithAfter 里,迁移成本几乎为零,但获得了更大的灵活性,这样我们不仅可以用withall去做事情,还能去获得更多的组合。
AfterScan我认为更像是特化钩子, 类似传统的继承式实现,我个人比较喜欢用组合代替继承,用显式代替隐式这种go风格。

@LanceAdd
Copy link
Copy Markdown
Member

好像没看到你代码中如合gdb使用的例子。
而且你的方式,可能和我的需求稍微有点偏差。
你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

AfterScan 是为 特定结构体 绑定 固定行为;而 gexecutor 是为 任意类型 提供 可组合的执行流程。 gexecutor是个泛型执行器,就如同你为指定结构体实现AfterScan方法一样,gexecutor也只需要为指定类型创建执行器,然后自己定义好beforeafter,剩下的你是想用gdb查询出这个还是自己直接new出来一个都随便,只需要withmain去定义一下就行。 你觉得不符合你需求是因为我举得例子都是用的int/struct之类的非指针类型,所以是不侵入的,如果需要before改变input输入以及after改变result输出,只需要输入输出都是指针类型就行了。这样你的BizItem 结构体本身不需要实现任何接口,保持纯净,你依然可以用 dao.Biz.Scan(&item),只是把 AfterScan 的逻辑挪到了 WithAfter 里,迁移成本几乎为零,但获得了更大的灵活性,这样我们不仅可以用withall去做事情,还能去获得更多的组合。 AfterScan我认为更像是特化钩子, 类似传统的继承式实现,我个人比较喜欢用组合代替继承,用显式代替隐式这种go风格。

每个人都会有自己的想法,我也只是抛出我的想法,剩下的可以看看其他小伙伴有没有更不一样的方案。

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Jan 23, 2026

好像没看到你代码中如合gdb使用的例子。
而且你的方式,可能和我的需求稍微有点偏差。
你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

AfterScan 是为 特定结构体 绑定 固定行为;而 gexecutor 是为 任意类型 提供 可组合的执行流程。 gexecutor是个泛型执行器,就如同你为指定结构体实现AfterScan方法一样,gexecutor也只需要为指定类型创建执行器,然后自己定义好beforeafter,剩下的你是想用gdb查询出这个还是自己直接new出来一个都随便,只需要withmain去定义一下就行。 你觉得不符合你需求是因为我举得例子都是用的int/struct之类的非指针类型,所以是不侵入的,如果需要before改变input输入以及after改变result输出,只需要输入输出都是指针类型就行了。这样你的BizItem 结构体本身不需要实现任何接口,保持纯净,你依然可以用 dao.Biz.Scan(&item),只是把 AfterScan 的逻辑挪到了 WithAfter 里,迁移成本几乎为零,但获得了更大的灵活性,这样我们不仅可以用withall去做事情,还能去获得更多的组合。 AfterScan我认为更像是特化钩子, 类似传统的继承式实现,我个人比较喜欢用组合代替继承,用显式代替隐式这种go风格。

明白你的意思了, 这的确也是一个不错的思路.

@wln32
Copy link
Copy Markdown
Member

wln32 commented Jan 23, 2026

好像没看到你代码中如合gdb使用的例子。
而且你的方式,可能和我的需求稍微有点偏差。
你例子的aop实在执行器上,约定的流程。 而我这里是落在需要scan的结构体上。这意味着,只要输出对象是这个结构体,afterscan的操作总能得到执行。 并且应该是能支持内嵌到其他同样具备该接口的结构体上。

我大概明白你的想法了,从你的需求来说你是为了简化代码,通过ORM提供高级特性来抹除大量的样板代码,但是实际上抽象出来应该是一种非侵入式的逻辑增强能力(当然也可以是侵入式的),在不修改原逻辑的情况织入额外的横切逻辑,这个就是解耦常用的类似前后增强/环绕增强的类aop能力或者装饰器模式。你的实现应该挺符合你的实际业务场景的,但是放在框架层面有点特化了,应该再抽象一下,提出来做成带有前后增强的通用执行器,这个执行器可以用于任何场景,无论是处理 HTTP 请求、数据库查询,还是简单的数据转换。我之前做过一个差不多的执行器 #4654 ,大家可以一起讨论下@gqcn @smzgl @hailaz @houseme @wln32

AfterScan 是为 特定结构体 绑定 固定行为;而 gexecutor 是为 任意类型 提供 可组合的执行流程。 gexecutor是个泛型执行器,就如同你为指定结构体实现AfterScan方法一样,gexecutor也只需要为指定类型创建执行器,然后自己定义好beforeafter,剩下的你是想用gdb查询出这个还是自己直接new出来一个都随便,只需要withmain去定义一下就行。 你觉得不符合你需求是因为我举得例子都是用的int/struct之类的非指针类型,所以是不侵入的,如果需要before改变input输入以及after改变result输出,只需要输入输出都是指针类型就行了。这样你的BizItem 结构体本身不需要实现任何接口,保持纯净,你依然可以用 dao.Biz.Scan(&item),只是把 AfterScan 的逻辑挪到了 WithAfter 里,迁移成本几乎为零,但获得了更大的灵活性,这样我们不仅可以用withall去做事情,还能去获得更多的组合。 AfterScan我认为更像是特化钩子, 类似传统的继承式实现,我个人比较喜欢用组合代替继承,用显式代替隐式这种go风格。

不能认同你的gexecutor方案来套用此思路,你所说的 【gexecutor 是为 任意类型 提供 可组合的执行流程】更适合用来做业务逻辑处理,不适合用来做库的一些方法动作的Hook
比如我要为几个不同类型在Scan或其他方法前后加一些我自定义的逻辑, 我提出的方案或者 @smzgl 的方案,注册一次或者实现说为类型实现接口即可,库在调用时自动判断有无实现,而你的gexecutor方案本质上跟我自己写一个方法然后调用Scan没什么区别,就目前来说为类型实现诸如AfterScan HookScan之类的接口不是一个通用的思路,你不能为第三方库类型实现这类接口,虽然可以通过定义新类型来实现,但并不是好做法
综上,我更推荐 @smzgl 或者说 我的方案 更适合

…hin the function to be within the same transaction as the caller.
@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Feb 3, 2026

大佬们,求关注,亲还问,能有结论吗?

@gqcn
Copy link
Copy Markdown
Member

gqcn commented Feb 11, 2026

@smzgl @wln32 @LanceAdd 这种设计增加了框架的隐含功能特性,使得组件过于复杂,业务上能处理的场景,就不需要在框架上加这种隐含功能特性了吧?我的建议是非必要的特性就尽量不加,以保持框架组件能力的聚焦,框架维护成本的高性价比。

@houseme
Copy link
Copy Markdown
Member

houseme commented Feb 11, 2026

是否可以先放到自定义驱动里,在使用上当作实验功能,先运行一段时间。

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Mar 30, 2026

@smzgl @wln32 @LanceAdd 这种设计增加了框架的隐含功能特性,使得组件过于复杂,业务上能处理的场景,就不需要在框架上加这种隐含功能特性了吧?我的建议是非必要的特性就尽量不加,以保持框架组件能力的聚焦,框架维护成本的高性价比。

以下仅是我个人不成熟看法以及对框架的理解, 说得不对的地方希望不要介意。

我认为作为框架其中一个重要作用, 就是框定架构, 让各种业务有章可循, 同时提供标准的数据流和基础功能, 从而减少很多不必要的重复代码. 在这个过程中, 通过接口、钩子、约定、注解、切面等各种方式把各个环节的暴露出来,以满足各种各种的业务场景。 对比grpc, gin等框架等, 总能在各个数据流转节点找到相关接口, 来切入自己的业务逻辑, 无论有些逻辑个人看来多么不合理, 但是对于特定场景下却又是当下最合适的选择.

回到我的场景, 想要解决的问题: goframe实现的ORM中,对表关联支持不是那么完善,特别是不希望硬编码表名称和表字段,以及不希望自行再重复声明一个跟entity差不多的结构体的时候。当有多级1对多表关联的时候, 我需要给前端返回一个干净的结构体. controller中各种循环赋值仅仅是为了给输出整形. 这是非常繁琐并且容易出错的, 特别是在业务多变的情况下. 而且这个接口恰好能解决我的问题.

你提到的业务上能处理的场景就在业务处理. "业务" 本身就是如何看待的问题, 我个人认为哪些属于业务哪些属于组件, 很多时候并不是那么的绝对. 每个业务总会把自己认为繁琐的, 可以复用的逻辑再次封装. 不断沉淀后, 何尝不是一个新组件呢?

@UncleChair
Copy link
Copy Markdown
Contributor

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
}

这个场景的写法感觉是一个变体的 MarshalJSON,本质上要处理的是业务字段名称的问题,针对这个加ScanHook感觉没有很大必要。另外Scan我理解是一个数据组装的过程,给这样一个过程加上before或者after hook会比较奇怪,如果是before,可以在数据查询阶段或者查询完成后做;如果是after,也大可以放进 MarshalJSON里,把这两个阶段抽象出来理论上确实没有什么问题,但是要加的话还是缺少一个实际只能这么写的应用场景,单纯的为了抽象把这两个阶段加上我觉得框架没有很大的实现必要。

真的优化的话我觉可能还是 with 更需要一点,毕竟是实验性特性。框架本身是没有模型关联设计的,也就导致很多查询没有太多比较简洁的写法,如果能实现类似:

type BizItem struct {
    *entity.BizItem
    CreatorName string `json:"creator_name" orm:"table:users with:id=creator_id field:name"`
}

这样的效果的话,丑是丑了点,但是你的场景能够得到很好的解决。

@smzgl @wln32 @LanceAdd @gqcn 不知道v3有没有新的 with 安排,关联查询这块感觉确实有提升空间。

@smzgl
Copy link
Copy Markdown
Contributor Author

smzgl commented Mar 31, 2026

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
}

这个场景的写法感觉是一个变体的 MarshalJSON,本质上要处理的是业务字段名称的问题,针对这个加ScanHook感觉没有很大必要。另外Scan我理解是一个数据组装的过程,给这样一个过程加上before或者after hook会比较奇怪,如果是before,可以在数据查询阶段或者查询完成后做;如果是after,也大可以放进 MarshalJSON里,把这两个阶段抽象出来理论上确实没有什么问题,但是要加的话还是缺少一个实际只能这么写的应用场景,单纯的为了抽象把这两个阶段加上我觉得框架没有很大的实现必要。

真的优化的话我觉可能还是 with 更需要一点,毕竟是实验性特性。框架本身是没有模型关联设计的,也就导致很多查询没有太多比较简洁的写法,如果能实现类似:

type BizItem struct {
    *entity.BizItem
    CreatorName string `json:"creator_name" orm:"table:users with:id=creator_id field:name"`
}

这样的效果的话,丑是丑了点,但是你的场景能够得到很好的解决。

@smzgl @wln32 @LanceAdd @gqcn 不知道v3有没有新的 with 安排,关联查询这块感觉确实有提升空间。

在MarshalJSON中, 实现结构体整型是一种方法. 但是这会有一些局限性:
一个是我的接口, 需要根据请求参数提供xml和json两种形式, 个别接口还需要csv格式. 具体的输出格式我在ghttp拦截器进行进行通用的序列化处理. 这意味着, 这类输出接口我需要实现3次.
第二个是在序列化函数实现整型, 意味着生成的swagger并不是按照实际输出来的, 因此多出来的字段并不会在swagger中体现. 为了满足swagger, 只能在结构体生成对应变量, 但是却不会赋值. 而这个结构体, 有可能会被service作为返回值返回. 调用方就用于出BUG, 明明有字段, 但是值却是空的.
第三个 这个写法不得不对表名和字段名进行硬编码. 如果这些发生了变更很难在编译期被发现.

题外话:

现在的表关联的确太弱, 光有with很多场景是无法满足的. 而用Join的情况下进行Scan, 不得不自己写一个包含两个表字段的结构体(这意味着字段名要硬编码), 而且现在的Join.Scan 在字段同名的情况下, 是有问题的, 除非使用as. 当两个宽表join的时候, 这是一个灾难.

我目前的一个解决方案是, 针对mariadb, 连接数据库的时候增加columnsWithAlias=true, select的时候会返回表名. 同时 gf gen dao 命令增加参数, 使得生成的entity字段也带上表名. 这样当使用Scan的时候就可以正确为下面结构体赋值.

type UserListItem struct {
*entity.User
*entity.Role
*entity.Dept
}

配置IAfterScan, 可以实现对表名和字段名零硬编码的情况下, 实现表关联后的结构体进行数据整形.

针对我目前需求多变, 设计多变, 表名字段名多变的情况下是比较省心和安全的. 毕竟只要重新执行gf gen dao, 任何数据库变动相关变动编辑器直接报错, 然后进行修改.

关于框架对外暴露切面切入点以及类似自定义序列化包这种需求社区内部讨论过很多次,需要调整很多东西,包括包的层级关系上浮下沉这种以及一系列调整,所以想要平滑兼容过度到V3基本不可能,所以才会有不向下兼容的V3计划,但是V2还是得继续先迭代开发修bug,最近因为大家都在捣鼓ai所以进度有点停滞等忙完这段时间就会处理积累的pr

@LanceAdd
Copy link
Copy Markdown
Member

LanceAdd commented Mar 31, 2026

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
}

这个场景的写法感觉是一个变体的 MarshalJSON,本质上要处理的是业务字段名称的问题,针对这个加ScanHook感觉没有很大必要。另外Scan我理解是一个数据组装的过程,给这样一个过程加上before或者after hook会比较奇怪,如果是before,可以在数据查询阶段或者查询完成后做;如果是after,也大可以放进 MarshalJSON里,把这两个阶段抽象出来理论上确实没有什么问题,但是要加的话还是缺少一个实际只能这么写的应用场景,单纯的为了抽象把这两个阶段加上我觉得框架没有很大的实现必要。

真的优化的话我觉可能还是 with 更需要一点,毕竟是实验性特性。框架本身是没有模型关联设计的,也就导致很多查询没有太多比较简洁的写法,如果能实现类似:

type BizItem struct {
    *entity.BizItem
    CreatorName string `json:"creator_name" orm:"table:users with:id=creator_id field:name"`
}

这样的效果的话,丑是丑了点,但是你的场景能够得到很好的解决。

@smzgl @wln32 @LanceAdd @gqcn 不知道v3有没有新的 with 安排,关联查询这块感觉确实有提升空间。

With/WithAll的优化我已经提交了一个 #4672 (等我这段时间忙完了再优化下)。然后针对于这种前后增强处理的需求我个人还是推荐在数据库查询外包一层,就像我之前回答里上传上来的例子 #4654 (纯为了这个issuse提交的,不会合并到框架里),当时我的具体回复可以往上翻一下,这种可复用的前后增强大家都能写出来。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

discuss We need discuss to make decision. feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants