Coder Social home page Coder Social logo

eorm's People

Contributors

archer-wyz avatar babynata avatar carolinezhang666 avatar codexiaoyi avatar deepsourcebot avatar flycash avatar flyhigher139 avatar heroyf avatar hookokoko avatar juniaoshaonian avatar jwcrystal avatar liangzai202204 avatar longyue0521 avatar skyenought avatar stone-afk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eorm's Issues

读写分离:MasterSlavesDB 实现

仅限中文

使用场景

这也算是一个实验性质的方案。目前我们有一个 Session 的抽象,那么我们可以考虑提供一个 MasterSlavesDB 实现来支持读写分离。

基本设计就是:

type MasterSlavesDB struct {
    master *sql.DB
    slaves *sql.DB
}

对于 SELECT 语句来说,默认情况下使用从库,其它语句默认使用主库。

但是有一个特殊的场景需要考虑,那就是在开启了事务的情况下,所有的查询都应该使用主库。那么相应的,我们需要修改已有的 Tx 结构体(或者提供一个新的实现),以保持这种语义。

另外还应该允许用户强制指定使用主库,但是不需要考虑用户要求强制使用某个从库的情况。

到目前为止,你不需要考虑从库负载均衡的问题,采用默认的轮询就可以,我们后面再考虑引入不同的负载均衡设计。

分库分表:merger 分页实现

仅限中文

使用场景

根据我的理解,分页的实现可以采用装饰器封装 #141 来达成。即在排序之后,进行分页。难度全在排序那边,分页本身非常简单。

ShardingSelector: 增强的 ShardingAlgorithm 设计与实现

仅限中文

使用场景

目前在合并请求 #145 里面我们初步解决了最简单的分库分表场景,即只考虑等值查询的条件,该如何生成SQL。

现在我们需要进一步强化 “分库分表” 规则这么一个概念。对于一个分库分表规则来说,它需要解决:

  • 根据输入的分库分表键的值,判断会命中哪个目标表
  • 在没有提供任何的分库分表键的时候,返回全部库和全部表,注意要返回类似于 "db.tbl" 这种形态的,而不能独立返回 []db, []tbl 这种

进一步说,如果在提供了分库分表键的情况下我们只能确定库,那么分库分表规则能够返回该库下面的所有的命中的表,也就是第一种情况的一个特例而已。

基本分库分表

但是进一步思考,我们会发现这个 ShardingAlgorithm 不是很好设计。因为用户需要的各种分库分表规则简直千变万化:

  • 简单的哈希分库分表:比如说按照 user_id % 32 来进行分库分表
  • 范围分库分表:比如说按照日期进行分库分表
  • 其它分库分表:这一类一般不需要按照哈希或者范围之类的来计算,可能就是用值来拼凑一下,或者判断一下标记位啥的;
    • 比如说按照国家或者地区来分库分表,比如说简单的写法就是 $region_user_db 这种表达式,那么当 region = cn 的时候,就变成了 cn_user_db。如果 $region 没指定,或者说部分数据没有按照地区分库分表,那么就会落到一个兜底的,比如说 user_db 里面
    • 按照压力测试来进行分库分表,也就是引入了所谓的影子库和影子表的问题,类似于 $shadow_user_db,如果是压测请求,那么就会命中 shadow_user_db。这种的特殊之处在于是不是压测请求,是需要从 context.Context 里面判断的
    • 按照 A/B 测试来进行分库分表:A/B 测试其实很少会使用不同的数据源,但是也防不住。所以在这种情况下它会类似于压力测试的案例

复合分库分表规则

很多时候,分库分表并不是只使用我们前面提到的那些基本分库分表的做法,而是可能涉及到多个组合在一起。

最典型的就是在压测的情况下,同时业务数据本身就是分库分表的。举个例子,假如说现在我们的生产库 user_db 是按照 user_id / 32 % 32 来分库,按照 user_id %32 来进行分表,那么对于生产库,我们可以用表达式写成:

user_db_$(user_id/32%32).user_tbl_$(user_id%32)

与之对应的影子库影子表则是:

shadow_user_db_$(user_id/32%32).shadow_user_tbl_$(user_id%32)

那么两个合并在一起则是:

$(shadow)_user_db_$(user_id/32%32).$(shadow_user)_tbl_$(user_id%32)

这里面有一个假设,就是影子库和影子表的分库分表规则和线上库的是一模一样的。但是有些时候有些公司不按套路来出牌,那么影子库和影子表的分库分表规则就是不同的,例如说生产库按照 32 来划分,而影子库影子表因为数据比较少,采用了 4 来划分,那么就变成了:

user_db_$(user_id/32%32).user_tbl_$(user_id%32)
shadow_user_db_$(user_id/4%4).shadow_user_tbl_$(user_id%4)

这个问题类似于后面我们提到的 zone 问题。这是一个很典型的场景:某一个分库分表的键取值,会影响其他分库分表键对应的分库分表逻辑

zone 问题

所谓 zone,简单直白的解释就是不同的机房,不同 zone 之间可能允许通信,也可能不允许通信。比如说一个国际大厂,它的 zone 可能分成美国 zone,东南亚 zone,** zone。当然这只是一个简单的例子,具体 zone 怎么划分都是各个公司根据自己的业务和合规情况来划分的。

那么很多时候业务方都会要求分库分表解决 zone 的问题,比较棘手的就是不同 zone 内部分库分表的规则又不同,比如说**大陆人口众多,那么按照 32 来分是合理的,而**作为一个省,屁大点人口,可能只需要按照 4 来分,于是我们就有两个:

cn_user_db_$(user_id/32%32).user_tbl_$(user_id%32)
tw_user_db_$(user_id/4%4).user_tbl_$(user_id%4)

一般来说,zone 的划分不太会影响分表。

那么也可以看出来,它本质上和影子库影子表面临的问题是一样的。

表达式问题

前面我已经多次提到了分库分表的一种形式表达,比如说在影子库影子表里面使用的:

user_db_$(user_id/32%32).user_tbl_$(user_id%32)
shadow_user_db_$(user_id/4%4).shadow_user_tbl_$(user_id%4)

那么这里就会有一个问题,我们的框架要不要支持这种表达式?以及如果支持的话,这种表达式应该如何定位,是应该认定为是一个框架必须要支持的核心功能,还是应该要认定为它只是组织分库分表表达式的一种形态?

那么很显然,既然我会问这个问题,而且根据我一贯的设计风格,那么我的答案是:表达式确实只应该作为一个扩展功能

也就是从设计上来说,即便分库分表不支持任何的表达式,那么用户依旧可以通过编程接口来指定自己的分库分表规则,类似于:

a := &MyShardingAlgorithm{}
err := registry.Register(&User{}, WithSharding(a))

那么为什么现在普遍分库分表都有类似的表达式呢?很简单,就是一个历史惯性而已。最开始的分库分表都倾向于为了减轻用户的接入难度。然而就我的观察来说,一些人很难学会怎么写这些表达式,比如说我在某处接到最多的问题就是这个分库分表该怎么写。

因此我认为表达式整体上不如编程接口。或者说,在提供了编程接口之后,我们没有十分强烈的动机去提供这么一种表达式解析的支持。

未来我们可以考虑支持。

分库分表对 DSN 的影响

注意,这里我们讨论的是 DSN(data source name),而不是 DNS。这里我依旧采用前面的这种表达式形式来阐述这个问题。

前面我举例的都是只影响到了 DB 名字和表名。那么从实际情况上来看,事情还要复杂一点。

比如说,这种分库分表规则:

$region.db.mycompany.com:3306/$region_user_db?timeout=123

那么显然我们的分库分表影响到了数据库的连接信息。

实际上,如果从最宽泛的角度谈,那么分库分表本身会影响 DSN 的任何一个部分。也就是说,包含端口、参数部分。

大多数情况下,一家公司内部如果使用分库分表的话,端口大多数时候都是同一个,不管你是分库还是读写分离的从库,都是使用一个固定的端口,比如说 3306,或者出于安全的考虑,大家都用另外一个,例如 4406 等。

分库分表对主从分离的影响

一般来说,如果一个公司准备采用分库分表的解决方案了,那么基本上可以认为这家公司肯定用了读写分离,也就是说数据库大概率都是一个主从集群。

所以我们在设计分库分表的时候要考虑到这个情况:

$region.db.master.mycompany.com:3306/$region_user_db?timeout=123
$region.db.slave.mycompany.com:3306/$region_user_db?tineout=456

实际上,公司可能有多个从库藏在这个从库的 DSN 背后,但是公司也可能一个从库给一个 DSN:

$region.db.master.mycompany.com:3306/$region_user_db?timeout=123
$region.db.slave1.mycompany.com:3306/$region_user_db?tineout=456
$region.db.slave2.mycompany.com:3306/$region_user_db?tineout=456

大体上我们认为,在分库分表的时候确定的应该是一个主从集群,至于这个主从集群内部究竟是怎么搞的,分库分表算法一点都不关心。

可行方案

我们的解决方案有一个非常核心的原则:分库分表中间件只知道接口,而不知道任何细节。形象点说,就是我把具体的分库分表算法,比如说哈希分库分表、范围分库分表或者复合分库分表算法从整个框架里面挪走,我的分库分表依旧能够正常运作。

这意味着:

  • 没有任何分库分表实现具有特殊地位
  • 分库分表接口的设计与实现本身可以独立出去作为一个单独的项目

实际上,因为常规来说我们一直说的都是分库分表,但是从前面的场景分析,我们应该能够看出来,严格说法应该是分集群分库分表。在这里我将 zone 之类的概念看做是一个是由集群衍生出来的业务规则上的概念,作为分库分表中间件,实际上没有什么zone,region 之类的概念。只有在特定的分集群分库分表规则实现里面会有

核心接口

核心接口只需要一个:

type Algorithm interface {
	// ShardingKeys 返回所有的 sharding key
	// 这部分不包含任何放在 context.Context 中的部分,例如 shadow 标记位等
	// 或者说,它只是指数据库中用于分库分表的列
	ShardingKeys() []string
	// Broadcast 返回所有的目标库、目标表
	Broadcast(ctx context.Context) []Dst
	// Sharding 返回分库分表之后目标库和目标表信息
	Sharding(ctx context.Context, req Request) (Result, error)
}

type Result struct {
	Dsts []Dst
}

type Dst struct {
	Name  string
	DB    string
	Table string
}

type Request struct {
	SkValues map[string]any
}

注意:

  • 返回的 Dst 里面包含了 DSN、DB 和 Table 三个字段。其中 Name 其实是一个值得斟酌的字段,它主要是为了解决 $(region).master.db.mycompany.com:3306/user_db 这种问题。或者说,它类似于别的框架中 data source 的概念;
  • 如果某个实现无法从 ctx 或者 req 里面找到任何跟定位分库分表有关的信息,那么就应该返回所有的候选的数据库和数据表。这也就是所谓的广播效果;
  • Broadcast 依旧引入了一个 ctx 作为输入,是因为我们要考虑类似于影子库上的广播要求。从理论上来说,它等价于 Sharding 方法里面没有传入任何的 sharding key。这个方法可以考虑去除;
  • 之所引入了 Request 和 Result 两个结构体,就是以防万一,如果我们将来需要扩展分库分表的功能,不至于连后悔药都没得吃;
  • 在 SkValues 里面我们使用了 any 来作为值类型,这意味着我们并不解决任何类型转换的问题。也就是意味着,用户在构造查询的时候,传入的是什么参数,我们就会原封不动往下传;
  • 在断定一个查询语句会命中哪个表的时候,会多次调用这个 Sharding 方法。比如说在按照 UserID 进行分库分表的场景下,类似于查询条件 WHERE (user_id = ?) OR (user_id = ?),则会调用两次 Sharding 方法

那么用伪代码来描述使用起来的效果则是:

meta := db.r.Get(t)
shardingResp := meta.algorithm.Sharding(req)
for _, dst := range shardingResp.Dsts {
ds := findDs(dst.Name)
ds.Exec(ctx, sql, args...)
}

用文字来简短描述则是:

  • 通过遍历 WHERE 里面的每一个查询条件(插入的话则是插入的元素),针对每一个条件调用 Sharding 方法
  • 根据 AND OR 的合并规则筛选目标节点
  • 根据目标节点中的 Name 找到对应的 Datasource,而后让对应的 Datasource 执行查询
  • 后续则是处理结果集

统一的 Datasource 抽象

我计划支持一个统一的 Datasource 抽象。它会有以下实现:

  • 单一的 DB,在 GO 里面,它的代表就是 sql.DB 实例
  • 主从集群,(+影子集群)
  • 分库分表集群,即包含了所有库所在的集群信息,这些集群信息可以考虑包含影子集群
  • zone 集群。如果将来真的有千奇百怪的 zone 之类的问题,那么我希望将对应的变更局限在这里

目前在第一期里面,这个不需要支持,后续再支持,我会额外创建 issue。

AND OR NOT

在引用了这个抽象之后,对 AND、OR、NOT 的支持是要发生变更的,但是我们保持已有的逻辑不变,只是在取并集或者合集的时候,将 Dst.Name 纳入考虑。即只有 Name, DB, Table 三者相等我们才认为是完全相同的。

但是在已有逻辑实现中,我们会碰到巨大的性能问题,即我们会频繁调用 Sharding 方法,引入额外的内存分配,这是我们后续要考虑优化的地方。

哈希实现

这里我们讨论一个简单的实现,即数据源相同的,或者说返回的 Dst 里面的 Name 都是同一个的情况。那么一个哈希实现我们很容易设计出来:

type Hash struct {
	Datasource   string
	Base         int
	ShardingKey  string
	DBPattern    string
	TablePattern string
}

func (h Hash) ShardingKeys() []string {
	return []string{h.ShardingKey}
}

func (h Hash) Broadcast(ctx context.Context) []Dst {
	panic("implement me")
}

func (h Hash) Sharding(ctx context.Context, req Request) (Result, error) {
	skValue, ok := req.SkValues[h.ShardingKey]
	if !ok {
		return Result{
			Dsts: h.Broadcast(ctx),
		}, nil
	}
	return Result{
		Dsts: []Dst{
			{
				Name:  h.Datasource,
				DB:    fmt.Sprintf(h.DBPattern, skValue.(int)%h.Base),
				Table: fmt.Sprintf(h.TablePattern, skValue.(int)%h.Base),
			},
		},
	}, nil
}

不过现实中一般哈希分库分表都不会那么简单粗暴,比如说它们可能用时前面的那种 DB/32%32 这种,那么简单修改这个 Hash 就可以提供一个新的实现。

基于范围的分库分表实现也是类似。但是基于范围的分库分表有一个地方比较恶心。比如说分表是按照日,分库是按照月,分集群是按照年。那么就意味着在转年的时候我们必须要有办法初始化一个新的数据源。

复合分库分表

我们可以简单写出来一个混合了影子库和哈希分库分表的:

func (h ShadowHash) Sharding(ctx context.Context, req Request) (Result, error) {
	skValue, ok := req.SkValues[h.ShardingKey]
	if !ok {
		return Result{
			Dsts: h.Broadcast(ctx),
		}, nil
	}
	if ctx.Value("shadow") == "true" {
		return Result{
			Dsts: []Dst{
				{
					Name:  h.Datasource,
					DB:    fmt.Sprintf("shadow_"+h.DBPattern, skValue.(int)%h.Base),
					Table: fmt.Sprintf(h.TablePattern, skValue.(int)%h.Base),
				},
			},
		}, nil
	}
	return Result{
		Dsts: []Dst{
			{
				Name:  h.Datasource,
				DB:    fmt.Sprintf(h.DBPattern, skValue.(int)%h.Base),
				Table: fmt.Sprintf(h.TablePattern, skValue.(int)%h.Base),
			},
		},
	}, nil
}

如果采用了影子表,或者影子集群,或者甚至于影子库和影子表的分库分表规则都不同,那么就做类似的修改

基于表达式的实现

我们可以考虑提供类似于其它框架支持的表达式的分库分表算法实现。

type Expr struct {

}

那么问题其实就剩下了表达式解析与字符串替换的问题了。比如说:

user_db_$(user_id/32%32).user_tbl_$(user_id%32)

那么也就是要把表达式 $(user_id/32%32) 提取出来,而且要知道知道 user_id 的值要从 SkValues 中拿到,然后再执行 /32%32。

后续考虑支持,第一期不支持。

测试

单元测试

单元测试必须覆盖以下所有的场景:

  • 只分表
  • 只分库
  • 同时分库分表
  • 只分 Datasoure
  • 分集群分库分表混合影子库影子表

以上所有的测试用例要进一步考虑 AND,OR,NOT 的效果

其它

多种分库分表规则

在一些特定情况下,用户对一张表都有多套分库分表,这种我们暂时不打算支持。我总体上认为是业务层面上设计不合理,所以中间件犯不着支持,毕竟我们是开源又不是公司内部项目,老板说支持就必须支持。

而且大多数情况下,用户可以通过组合 Algorithm 来达成类似的目标。

其它语句

正如我之前说过的,我们并不打算解决所有的问题。我们这里主要集中解决增删改查,而且是对业务数据的增删改查。

那么一些特别的语句,比如说 CREATE USER 之类的。当然并不是说不能执行,而是说犯不着特意在分库分表内核为它们留下接口。

只是说随缘,如果不需要额外的努力也恰好可以支持,那就支持;如果需要额外的努力,那么我们就不支持。

审慎思考核心与非核心

前面梳理了不同的分库分表场景之后,我要强烈批评两个错误设计:

  • 给予某一类分库分表特殊地位的设计
  • 认为分库分表表达式是分库分表核心功能的设计

就第一个问题来说,我认为很多分库分表中间件都犯了这个错误。比如说对哈希分库分表进行了特殊处理,或者说给予了特殊的地位。甚至于在设计表达式的时候,都给予了它特殊的地位。

这里我要提及一个案例,就是某司的分库分表中间件,欠缺一个对分库分表算法的一个抽象。后来果然在扩展支持一些功能的时候遇到了问题。比如说另我印象很深刻的就是支持 zone 概念的时候,在分库分表中间件中引入了和 zone 相关的很多概念。

他们的做法不同于我在这里提及的把 zone 等概念限定到某一个具体的分库分表算法的实现里面,而是在分库分表核心内部就引入了很多和 zone 相关的东西。

这就是典型的巨大的设计错误。而且在可以预计的未来,他们还会遇到更多的困难。

当然这并不是这家公司这部分开发人员才会犯下的错误,而是大多数设计者在设计中间件都会犯的错误。

常见的原因则是中间件研发者往往会受到业务研发的影响。一个业务研发认为重要的功能,中间件研发者很容易被误导,做成框架的核心功能。

而实际上,业务上认为重要的功能并不等于中间件要解决的核心问题。那么对于一个研发者来说,他就要审慎思考当公司要求中间件提供一个功能的时候,这个功能究竟是不是中间件的核心功能。

valuer/reflect: 用 Field 取代 FieldByName

仅限中文

使用场景

在 ORM 的底层实现里面,解析结构体的值有两种实现,一种是 unsafe,一种是 reflect。显然 unsafe 大多数时候性能会很好,尤其是在组合的情况下。

但是目前的 reflect 的实现也没有达到最佳状态,例如:

// Field 返回字段值
func (r reflectValue) Field(name string) (any, error) {
	res := r.val.FieldByName(name)
	if res == (reflect.Value{}) {
		return nil, errs.NewInvalidFieldError(name)
	}
	return res.Interface(), nil
}

func (r reflectValue) SetColumns(rows *sql.Rows) error {
	cs, err := rows.Columns()
	if err != nil {
		return err
	}
	if len(cs) > len(r.meta.Columns) {
		return errs.ErrTooManyColumns
	}

	// TODO 性能优化
	// colValues 和 colEleValues 实质上最终都指向同一个对象
	colValues := make([]interface{}, len(cs))
	colEleValues := make([]reflect.Value, len(cs))
	for i, c := range cs {
		cm, ok := r.meta.ColumnMap[c]
		if !ok {
			return errs.NewInvalidColumnError(c)
		}
		val := reflect.New(cm.Typ)
		colValues[i]=val.Interface()
		colEleValues[i] = val.Elem()
	}
	if err = rows.Scan(colValues...); err != nil {
		return err
	}

	for i, c := range cs {
		cm := r.meta.ColumnMap[c]
		fd := r.val.FieldByName(cm.FieldName)
		fd.Set(colEleValues[i])
	}
	return nil
}

都使用了 FieldByName,但是我们有更好的选择 Field

我对 FieldFieldByName 两个做了性能测试,效果如下:

func BenchmarkFieldIndexOrName(b *testing.B) {
	tm := TestModel{}
	val := reflect.ValueOf(tm)
	b.Run("by index", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			// 随便取一个,差异不大
			_ = val.Field(3)
		}
	})

	b.Run("by name", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			// 随便取一个,差异不大
			_ = val.FieldByName("Age")
		}
	})
}

在我电脑上执行测试 go test -bench=BenchmarkFieldIndexOrName -benchmem -benchtime=10000x,得到的结果是:

BenchmarkFieldIndexOrName/by_index-12              10000                 2.840 ns/op           0 B/op          0 allocs/op      
BenchmarkFieldIndexOrName/by_name-12               10000                71.07 ns/op            8 B/op          1 allocs/op      
PASS

可以看到,by index 远比 by name 快。

所以为了进一步提高性能,我希望能够将这两个地方的 FieldByName 都替换为 Field

为了达到这个目的,还需要修改我们的 TableMeta ,在字段里面增加一个 index 字段。暂时不考虑组合情况,我们可以把它定义成:

// ColumnMeta represents model's field, or column
type ColumnMeta struct {
	ColumnName string
	FieldName    string
	Typ             reflect.Type
	IsPrimaryKey    bool
	IsAutoIncrement bool
	// Offset 是字段偏移量。需要注意的是,这里的字段偏移量是相对于整个结构体的偏移量
	// 例如在组合的情况下,
	// type A struct {
	//     name string
	//     B
	// }
	// type B struct {
	//     age int
	// }
	// age 的偏移量是相对于 A 的起始地址的偏移量
	Offset uintptr
	// IsHolderType 用于表达是否是 Holder 的类型
	// 所谓的 Holder,就是指同时实现了 sql.Scanner 和 driver.Valuer 两个接口的类型
	IsHolderType bool

        Index int
}

那么在解析 ColumnMeta 的时候,同时要把这个 Index 设置好。

要求:

  • 支持 ColumnMeta 里面新增字段 Index, ColumnMeta 在internal/model 包中
  • 重构 internal/valuer 的 reflectValue 实现,将 FieldByName 替换为 Field

eorm: 支持基本类型作为返回值

仅限中文

使用场景

在有一些场景下我们会在 SELECT 里面使用聚合函数,例如:

SELECT AVG(`age`) FROM users 

在这种情况下,我们会希望 Selector 能够返回基本类型:

avgAge := NewSelector[int].Select(xxx).From(&User{}).Where(xxx).Get(ctx)

相比我们已经支持的用法,它的改动主要涉及到:

  • 表名:早期我们是可以直接从泛型的类型参数里面解析得来,现在我们只能考虑引入 From 方法,并且 From 里面不再是接收一个 string 参数,而是一个结构体参数
  • 结果集处理:在这种情况下,我们只能允许用户查询一个列,多余的列我们会返回错误。而这个单独的列,就要转化为基本类型

除了 Get,显然还需要支持 MultiGet。

而要支持的类型包括:

  • string
  • 基本类型
  • []byte

行业分析

在 Go 里面基本没有什么可以参考的东西,因为这是第一次采用泛型来解决聚合函数的问题。

可行方案

实际上,如果我们不考虑已有的 valuer 抽象的话,实现起来还是很容易的,我们只需要在 Get 和 MultiGet 里面进行类型判断,而后直接调用 Scan 方法,这也就是最简单的方法。

我大概评估了一下,如果我们希望将这个逻辑下沉到 valuer.SetFields 方法里面,那么还是比较棘手的。这种做法的好处是保持住了 valuer 抽象。

其它

  • 你可以先提供一个草稿合并请求,它不需要有测试,然后我们在这个合并请求下讨论你的设计的优缺点
  • 如果你是极客时间中实战训练营的学员,你可以直接联系我,我可以约一个会议详细解释这个需求。否则你可以考虑给我发邮件,预约一个时间,我也可以详细解释这个需求

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

读写分离:为 MasterSlavesDB 添加单元测试

仅限中文

使用场景

因为在分库分表的 ShardingSelector 里面我们需要用到 MasterSlavesDB,所以我先合并了合并请求,但是针对 MasterSlavesDB 的单元测试不够完善,所以我们还需要进一步提供单元测试。

为了支持单元测试,我们可以考虑使用 mock 的 Slaves 实现。这样可以确保我们一定走过去了主节点或者从节点。

项目缺少代码格式化工具

仅限中文

使用场景

项目缺少格式化工具,详见 #82

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

重构:删除 IsHolderType

仅限中文

使用场景

在 GO sql 包里面有两个很特殊的接口:sql.Scanner 和 driver.Valuer。前者用于接收数据库返回的数据,后者用于返回查询的参数。典型的使用场景就是 sql.NullXXX 这些类型。

那么实现了这两个接口的类型就不能被当做普通的类型处理。因此在 ColumnMeta 里面设置了一个标记位 IsHolderType,目前来看这个标记位有点多余,所以可以删掉。

核心还是在于我简化了整体实现,所以已经用不上了。

Buffer pool

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

When we are building a query, we can use strings.Builder. But in fact, we should think about reuse the buffer to avoid GC and improve the performance.

Usually, we will use sync.Pool to cache the buffer. But there is a problem that the buffer's capacity may be expanded when it was put back.

For example, the original capacity of buffer is 128, and it means that we can only store 128 bytes. However, we reuse this buffer to build a long query which has more than 128 bytes, e.g. 200 bytes, and then the buffer capacity was expanded to 256 bytes.

If most of the queries only need 50 bytes, it means that we waste a lot memory. A typical solution is avoiding cache big buffer.

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

Inserter

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Inserter is used to build INSERT query, we have already defined API.

In this issue, you don't need to support upsert, and complex expressions.

(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

relies on #10

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

分库分表:Datasource 抽象

设计

在前面的 #157 里面我提到了 Datasource 抽象。它主要是为了提供一个统一的对数据源的抽象的。

基本上我们可以预期到,在实际中存在的 Datasource 会有多种可能:

  • 简单的单一数据库
  • 主从集群
  • 主从集群 + 对应的影子集群

如果我们进一步从逻辑上考虑,那么分库分表也可以认为是一个 Datasource,而后在其内部根据选中的集群、库和表进行分发。

此外,我们可以预期这些不同的 Datasource 是可以进行组合的。假如说我们有一个叫做 ShardingDatasource 的东西,那么我们可以预期,它内部可以包含多个主从集群,那么每个主从集群本身就是一个主库叠加多个从库这种简单的单一数据库。

更进一步说,在压力测试经常讲的影子库影子表也可以进一步被组合进去。

我们可以给 Datasource 提供一个统一的定义:

type Datasource interface {
	Query(ctx context.Context, query ShardingQuery) (*sql.Rows, error)
	Exec(ctx context.Context, query ShardingQuery) (sql.Result, error)
}

其中 ShardingQuery 定义为:

type ShardingQuery struct {
	SQL  string
	Args       []any
	Datasource string
}

这种设计有一个很明显的缺陷,就是 query 本身被限定为和 Datasource 有关的概念了。

ShardingDatasource

可以定义为:

type ShardingDatasource struct {
	datasources map[string]Datasource
}

func (s *ShardingDatasource) Query(ctx context.Context, query ShardingQuery) (*sql.Rows, error) {
	ds, ok := s.datasources[query.Datasource]
	if !ok {
		return nil, errors.New("datasource not found")
	}
	return ds.Query(ctx, query)
}

func (s *ShardingDatasource) Exec(ctx context.Context, query ShardingQuery) (sql.Result, error) {
	//TODO implement me
	panic("implement me")
}

那么很显然,ShardingDatasource 一点都不关心具体的一个 Datasource 究竟是单一的 DB,还是一个主从结构,还是说混合了影子库影子表的东西。

MasterSlavesDatasource

对于主从数据源来说,可以定义为:

type MasterSlavesDatasource struct {
	master *sql.DB
	slaves slaves.Slaves
	core
}

注意这里值得斟酌的是 master,可以考虑将类型从 sql.DB 换成 Datasource 本身,那么就可以层层嵌套,形成类似于千层饼的结构了。

影子库和影子表的支持

理论上来说,影子表只能从分库分表规则里面在生成的 SQL 里面将表名换成影子表名。

但是如果是影子库,那么其实有很多种做法,可以考虑定义一个全新的 ShadowDatasource,里面维持住 live 和 shadow 两个 Datasource

type ShadowDatasource struct {
	live   Datasource
	shadow Datasource
}

那么也可以考虑说增强我们前面的的 MasterSlavesDatasource,在里面嵌入一个 shadow 的字段:

type MasterSlavesDatasource struct {
	master *sql.DB
	slaves slaves.Slaves
	shadow Datasource
}

当然,在 ShardingDatasource 里面增加一个 shadows 字段也不是不可以。

所以本质上,shadow 还是非 shadow,并不是分库分表内核需要管理的东西。

zone 支持

简单来说,可以认为一个 zone 就是一个 Datasource 实例,而后多个 zone 对应的 Datasource 可以合并在一起构成一个业务上有意义的单位。

比如说订单表,那么可能有** zone,可能有美国 zone,那么分库分表无非就是要确认某一个请求是发给美国 zone 还是发给** zone。

当然,如果是彻底的隔离机制,那么我们可以做到说,在初始化 zone 所代表的 Datasource时,我们只初始化所在 zone 的那个实例。

而不管如何,这些事情都可以被限制在 Datasource 的某一个实现之类。依旧可以确保说整个分库分表的核心是和 zone 没有什么关系的。

其它

已有的 Session 概念

不得不说,这是我借鉴得非常失败的一个概念。本身源自某 ORM 框架的概念,但是现在我看来,Session 应该被 Datasource 取代。

之前是顾虑到有一类场景,用户可能希望把一些的操作放在一个上下文内,在这个上下文内他们可以考虑设置一些共享的设置,比如说链接参数、ORM 的 middleware 等。

但是现在看起来,完全没有这个必要。

接下来我们还会保留这个 Session 概念,但是会进一步淡化它的作用。

eorm: Delete 的执行

仅限中文

使用场景

很显然,到目前为止我们只支持了 SELECT 和 INSERT,在 DELETE 语句上,我们只支持了构造出来 DELETE 语句,但是没有支持发起 DELETE 查询并且返回结果。

DELETE 语句和 INSERT 语句在执行上并没有什么差距,所以可以直接将 Inserter 的执行代码复制一份过来。

但是你需要提供:

  • 基于 sqlmock 的单元测试
  • 集成测试

读写分离:Slaves 基于 DNS + 轮询的实现

仅限中文

使用场景

正常来说,在一家大规模的公司里面,我们不太可能直接指定所有的 slaves,因为从库可能都有数十个。

具体来说:

  • 某些公司的所有从库,DBA 都会告诉你 IP 和端口,而后你自己配置,也就是目前我们默认 roudrobin 实现;
  • 某些公司引入类似代理之类的机制,那么 DBA 只是提供一个从库的 DSN,而本质上这个 DSN 指向的是这个代理,DBA 会在这个代理的基础上去做监控和负载均衡等
  • 某些公司直接依赖于域名解析(DNS),而后 DBA 就是给你一个 DSN,例如 slaves.mycompany.com:3306/my_db,DBA 要求你自己去 DNS 服务器上查询该域名,拿到所有的可用 IP 列表

第二种我们不需要管,但是我们需要为第三种提供一个实现。这个实现的功能非常清晰:

  • 查询 DNS 获得所有的可用从库 IP
  • 每次查询对从库进行负载均衡,这里我们不考虑复杂的负载均衡机制,直接采用轮训
  • 定时轮询 DNS 更新可用 IP

如果考虑到容错,那么需要:

  • 如果一个特定的从节点连续 N 次查询都失败,那么要把该从节点从可用节点剔除出来,或者理解查询一次 DNS,确保该从节点没有下限;
  • 如果一个从节点很久没有被查询到(往往意味着你的服务器负载不高),那么需要发送心跳,确保这段时间从库并没有宕机,或者下线

整个过程,类似于基于 DNS 的服务发现和负载均衡。

容错是一个非常复杂的话题,你可以创建一个草稿合并请求,我们在那个合并请求里面具体讨论。

重构:取消支持 Having 中使用 alias

仅限中文

使用场景

之前在尝试支持 HAVING 的时候,我支持了这种语法形态:

select AVG(age) as avg_age from xxx group by xx HAVING avg_age < 100

现在经过思考之后,我觉得这种语法形态是不必支持的,因为:

  • HAVING 本身就用得不多,而在 HAVING 里面使用这种别名的写法就是更是少见了
  • 用户可以直接使用 HAVING AVG(age) < 100,其性能在大多数的数据库上都没有什么区别

取消支持只需要删除 builder 结构体里面的 aliases 字段,而后调整对应的代码和测试用例就可以了。

type builder struct {
	core
	// 使用 bytebufferpool 以减少内存分配
	// 每次调用 Get 之后不要忘记再调用 Put
	buffer  *bytebufferpool.ByteBuffer
	meta    *model.TableMeta
	args    []interface{}
	aliases map[string]struct{}
}

Metadata: table metadata and columns metadata

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Metadata is the facility of EQL. All other QueryBuilder depend on the metadata to know the table name, columns name.
(Please state use cases, what problem will this feature solve?)

Proposed Solution

We need some structures to represent the table and column:

type TableMeta struct {
}

type ColumnMeta struct {
}

TableMeta at least contains those information:

  • table name
  • columns
  • primary key

ColumnMeta at least contains those information:

  • column name
  • field name
  • type
  • is primary key
  • auto increment

In order to ensure EQL able to work with framework, we need an abstraction representing how to get the metadata. We call it MetaRegistry:

type MetaRegistry interface {
    Register(model interface{}, opts...MetaOption) TableMeta, error
    Get(model interface) TableMeta, error
}

We provide the default implementation based on tag.

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

Predicates Support

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

(Please state use cases, what problem will this feature solve?)

Predicates are used to filter rows. Predicate can be used in WHERE clause.

Proposed Solution

(Please describe your proposed design/solution, if any)
Here are the major APIs and you can find them in predicate.go:

// P creates a Predicate
// left could be string or Expr
func P(left interface{}) Predicate {
	panic("implement me")
}

// Not indicates "NOT"
func Not(p Predicate) Predicate {
	panic("implement me")
}

// And indicates "AND"
func (p Predicate) And(pred Predicate) Predicate {
	panic("implement me")
}

// Or indicates "OR"
func (p Predicate) Or(pred Predicate) Predicate {
	panic("implement me")
}

// EQ =
func (p Predicate) EQ(val interface{}) Predicate {
	panic("implement")
}

// LT <
func (p Predicate) LT(val interface{}) Predicate {
	panic("implement me")
}

// GT >
func (p Predicate) GT(val interface{}) Predicate {
	panic("implement me")
}

But the most important things is to convert the predicates to string. For example:

P("id").LT(10)// `id` < 10

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

It relies on #10

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

重构:删除 IsAutoIncrement 字段

仅限中文

使用场景

早期在考虑支持增删改查的时候,我认为允许用户指定主键是一个很合理的事情,因为我们可以考虑支持诸如 UpdateByPK 之类的方法。

但是目前来说,我成功规避了所有类似的设计,因为本身我在维护 Beego ORM,或者审视 GORM 的设计的时候,我认为 ORM 框架完全不应该关心一个列是不是主键。

那么类似于 UpdateByPK 这种调用,用户可以直接在 Where 方法里面指定 C("Id").EQ(123)。

这有两种效果:

  • 对于用户来说他们的代码可读性和可维护性更高,因为他们清楚知道他们就是利用主键来作为查询条件
  • 对于 eorm 的代码本身来说,我们也不再需要关系究竟是不是主键了,完全按照用户的指令来执行查询

所以我现在希望删除在 ColumnMeta 里面的 IsAutoIncrement 字段,而后修改对应的代码和测试用例。

Like clause

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

LIKE is simple. But I am thinking about, should we provide functions "HasSuffix, HasPrefix, Like".

(Please state use cases, what problem will this feature solve?)

Proposed Solution

At least we need one function:

func (p Predicate) Like(str string)

These functions are open to discuss:

func (p Predicate) HasPrefix(str string)
func (p Predicate) HasSuffix(str string)
func (p Predicate) Contains(str string)

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

eorm: Result 抽象

仅限中文

使用场景

目前来说,Inserter, Deleter #92 和 Updater #93 的 Exec 方法都会返回一个 (sql.Result, error),
但是实际上用起来很恶心:

res, err := i.Exec(ctx)
if err != nil {
    return nil, err
}
id, err := res.LastInsertId()
if err != nil {
    return nil, err
}
// id 处理一下 ID 啥的

所以这里就会出现两次 error 的判断。实际上我们完全可以考虑返回一个 sql.Result 的实现,类似于:

res := i.Exec(ctx)
id, err := res.LastInsertId()
if err != nil {
    
}

行业分析

这里面可以参考 sql.Row 结构:

// Row is the result of calling QueryRow to select a single row.
type Row struct {
	// One of these two will be non-nil:
	err  error // deferred error for easy chaining
	rows *Rows
}

也就是将原本的返回值 (*Rows, error) 封装成了一个结构体。

可行方案

定义一个我们自己的 Result 结构体:

type Result struct {
    err error
    res sql.Result
}

它会实现 sql.Result 接口,同时暴露一个 Err() 方法,用于返回 err 字段。

其它

  • 如果 #93 #92 已经解决掉了,那么你需要同时修改 inserter, deleter 和 updater 的代码
  • 提供单元 Result 本身的单元测试
  • 修改对应的单元测试和集成测试

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Ignore Columns

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

(Please state use cases, what problem will this feature solve?)

Sometimes users define a complicate structure but some of its fields should not be inserted into databases. It indicates one thing: some fields can not be mapped to columns.

In some small applications, users do not use entity or PO. They only have one layer named model, and they use models in their business code as well as DAO.

So we need to allow users to declare that they want EQL to ignore some field.

Proposed Solution

(Please describe your proposed design/solution, if any)

We already have a MetaRegistry named tagMetaRegistry, so we can use the syntax: eql:"-" to ignore the columns. Besides, we need to provide an option to ignore some fields when users register models.

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

eorm: LIKE 查询

仅限中文

使用场景

在搜索列的时候,我们会考虑检索字符串是否符合某个特征,在这种情况下,我们可以考虑使用 LIKE 查询。
例如:

SELECT * FROM XXX WHERE XXX LIKE '%abc'

行业分析

LIKE 查询在很多中间件里面都提供了支持,并且它们还进一步封装了:

  • HasPrefix:等效于 LIKE '%abc',用户使用起来是 HasPrefix(abc)。注意,%是框架帮助添加上去的
  • HasSuffix: 等效于 LIKE '%abc',用户使用起来是 HasSuffix(abc)。注意,%是框架帮助添加上去的

可行方案

提供一个 LIKE 调用,接近于 #101 #102 中的设计:

C("name").Like("abc")
C("name").NotLike("abc")

这里 Like 和 NotLike 接收字符串类型。另外就是我们也不对 name 的类型进行校验,例如即便用户使用的是 int 类型,他要用 Like 也是可以的。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Precalculate offset of field and use `unsafe` to read data.

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Please take note Enhancement is a suggestion to existing features and codebase.
If you're requesting for a new feature, please select Feature.

Summary

Now, several APIs rely on reflection package to fetch the value by field name. But I think we can use unsafe instead. Here is the point:

  • When we initiate model's metadata, we know the offset of each fields
  • When we read from data of specific field, we use this offset and rebuild the data based on its type
  • We need benchmark tests to compare unsafe and reflection

(State a summary of your enhancement suggestion)

What version of EQL are you using?

What version of Go are you using?

(Get your go version by go version)

What is the current design/solution/implementation?

(If you can, link to the line of code that could be improved)

What could be made better?

(What you think should be enhanced and how?)

分库分表:ShardingSelector 实现

仅限中文

使用场景

这个作为初步实现,用来验证我们的设计。根据我们讨论的内容,ShardingSelector 牵涉到:

  • 改写 SQL:目前的话我们可以假设只会有 sharding_key = ? 这种形式,如果没有 sharding_key,我们将返回错误,而不是执行广播;
  • 执行 SQL:直接发起发起查询。但是对于 Get 和 GetMulti 来讲是有点区别,如果是 Get,那么我们要确保前面的改写 SQL 只能生成一个 SQL(目前加上这种严苛的校验);如果是 GelMulti,那么就需要开启 goroutine 并发查询不同的物理表
  • 合并结果集:可以复用 merger 里面批量查询

所以很显然:

  • 你要考虑怎么允许用户配置分库分表规则,这部分我们目前的想法是加入进去模型元数据里面,所以你需要小心设计接口;
  • 用户必须主动调用 registry.Register 方法来注册模型,指定分库分表的规则。目前我们不打算支持在 eorm 标签里面使用表达式;

依赖于 #140

Having Clause

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

"HAVING" clause is similar to "WHERE" but HAVING can use aggregate functions. There are two kinds of SQL:

SELECT * FROM xxx HAVING AVG(`price`) > 100;
SELECT AVG(`price`) as avg_price FROM xxx HAVING `avg_price` > 100;

Now, #12 will define several aggregate functions. We need to reuse those aggregate functions.
For example:

var p Predicate = Avg("Price").LT(100)

But if users use alias:

var p Predicate = Avg("Price").As("abc").LT(100) // this should be invalid

(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

eorm: 插入忽略主键

仅限中文

使用场景

在插入的时候,如果用户主键使用的是基本类型,那么我们依旧会插入基本类型的零值,例如:

type User struct {
    Id uint64
}
NewInserter(db).Values(&User{})... // 会插入主键为 0 的 user

而实际上用户是希望忽略主键的。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

核心是要告诉 Inserter 忽略主键。从理论上来说有两种思路:

  • 在用户指定 Columns 的时候,提供一个辅助方法,叫做 NonPKColumns(entity)[]string,返回 entity 对应的模型的所有的非主键的列;
  • 一个是在 Inserter 上引入一个叫做 IgnorePk 的选项

从我个人的使用偏好上来说,我更加喜欢第一个方案,但是可能用户更加喜欢第二个选项

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

分库分表:Merger 抽象与批量查询实现

仅限中文

使用场景

我们将启动新的分库分表特性支持。

为了支持分库分表,一个重要的抽象就是处理结果集,这个抽象被我命名为:

type Merger interface {
    Merge(ctx context.Context, results []*sql.Rows) (sql.Rows, error)
}

它会作为一个非常独立的模块,暂时放进去 internal/merger 包里面。

整个 merge 包必须完全独立,即不依赖与 eorm 的其它任何包,后面我会考虑将它拆出去作为一个独立的项目。这主要是因为后面我不仅考虑结合 ORM 来做分库分表,还想设计一个纯粹的分库分表的 driver,或者一个分库分表的 proxy,sidecar 等。

同时,你还需要提供一个最基本的实现,即批量查询的实现。

例如我们可以预期一个查询 select * from user_tab where id in (1, 2, 3) 那么会被分库分表成三个查询,这个实现就是将这三个查询的结果汇总在一起。

但是需要注意的是,该实现要考虑性能问题,即如果多个查询总共返回数十万行数据,该如何处理。核心就是要避免自己提前调用 Next 将所有的数据都取过来,然后缓存在内存里面,而是应该在用户调用 Next 的时候再调用一个 results 里面特定的 sql.Rows 的 Next 方法。

eorm: IN 查询支持

仅限中文

使用场景

在 WHERE 部分使用 IN 查询。目前来说,我们暂时不需要考虑子查询的问题,因为子查询的形态不同于普通的 IN 语句。

可行方案

// 用户用起来
s.Where(C("Id").In(1, 2, 3)) // WHERE `id` IN (?, ?, ?)
s.Where(C("Id").NotIn(1, 2, 3)) // WHERE `id` NOT IN (?, ?, ?)

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Updater

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Support UPDATE query. In this phase, you don't need to support complex expression and LIMIT clause.
(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

relies on #10

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

IN and NOT IN

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

IN and NOT IN are different from other Predicate because they take slice or array as input.

Here is a point that I am not sure: should we support map? It's similar to:

// In(m)
values := valuesFromMap(m)
In(values)

Now, we don't need to take subquery into consideration.

(Please state use cases, what problem will this feature solve?)

Proposed Solution

Define two functions:

func (p Predicate) In(val interface{})
func (p Predicate) NotIn(val interface{})

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

Relies on #11

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

CI: golant-ci lint 检查

仅限中文

使用场景

由讨论 #82#84 启发而来,我们需要检查代码 linter 问题。

目前我们现在已经集成了 deepsource 和 codecov 两个 action,从理论上来说它们都支持静态代码质量检测。但是我并不确定它们是否有缺漏。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

目前市面上做静态代码检测的工具有很多:

  • deepsource:比较全面的静态代码检测
  • codecov:这个主要是做代码覆盖率检测
  • travis:也是类似的工具,不过早期 travis 中间做过一次迁移,所以在我们这个项目并没有启用
  • golantci-lint:算是一个在 Go 语言里面被广泛使用的工具,我们可以预期这个工具能够在 Go 版本变化的时候及时更新

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

事实上,在我们的 make setup 命令里面已经把 golangci-lint 放到了 pre-push 里面,所以也就是如果贡献者按照标准初始化了开发环境,那么他们推送代码的时候就会触发。

但是我们依旧需要一个 github action 来做检测,以确保我们的贡献者确实做了这些检查。

集成 golangci-lint-action

要额外开启 [asasalint](https://github.com/alingse/asasalint)

其它

任何你觉得有利于解决问题的补充说明

任何需要补充额外开启的 linter 可以在这里补充

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

eorm: GetMulti 批量查询支持

仅限中文

使用场景

我在用的时候我才发下,我居然没有实现 GetMulti 方法。

func (s *Selector[T]) GetMulti(ctx context.Context) ([]*T, error) {
}

实现这个方法可以参考 Selector 上的 Get 方法,并且核心逻辑,也就是处理结果集的地方,是可以复用的。

有一个地方不太一样,即 GetMulti 本身不会返回 ErrNoRows 的错误,因为我们认为在批量查询的时候,没有结果是一个很正常的事情。这也是为了和 sql 包的语义保持一致。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

分库分表:merger 排序实现

仅限中文

使用场景

在处理结果集的时候,排序是一种常见的情况。例如逻辑上的 SQL 是 SELECT * FROM user_tab WHERE id > 100 ORDER BY id ASC

那么基本上会将请求发送到非常多个表上。在处理结果集的时候,需要将这些返回的全部结果再进行一次排序。

从实现角度来说:

  • 需要知道按什么来排序,是升序还是降序;
  • 在知道按什么来排序的情况下,需要考虑排序列的类型,例如 id 是数字或者 id 是字符串,那么排序的算法是不一样的;
  • 要综合考虑多个列排序的情况,例如 id ASC, create_time DESC

注意,用户必须指定排序的列,如果没有指定则是报错。这意味着我们在这层面上并不会默认使用主键来进行排序。

eorm: AOP 接口设计

仅限中文

我在极客时间的实战训练营里面已经讲得很清楚了。设计文档后续补,TODO

使用场景

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Aggregate functions

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

We need to support major aggregate functions, check MySQL aggregate functions

We only support a few of them.
(Please state use cases, what problem will this feature solve?)

Proposed Solution

Here is an example:

// As specifies the alias
func (Aggregate) As(alias string) Aggregate {
	panic("implement me")
}

// Avg represents AVG
func Avg(c string) Aggregate {
	panic("implement")
}

You need to think about this:

New().Select(Columns("Id"), Avg("Age").As("avg_age")).From(&TestModel{}) //  "SELECT `id`, AVG(`age`) as `avg_age` FROM `test_model`;",

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

It relies on #10

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

Question: Merger 抽象相关逻辑语义问题及批量查询实现问题

抽象定义问题

当前Merger和Rows抽象定义如下

type Merger interface {
	Merge(ctx context.Context, results []*sql.Rows) (Rows, error)
}

type Rows interface {
	Next() bool
	Scan(dest ...any) error
	Close() error
	Columns() ([]string, error)
}
  1. Rows中方法是sql.Rows的方法的子集,方法签名相同但逻辑语义却不同(见下方MergerRows实现问题),在后续使用Rows会让人困惑。是否可以让Rows中与sql.Rows相同的方法保持一致的逻辑语义,既符合直觉使用者不用改变习惯也能是代码根据扩展性。换句话说,重新定义Rows语义达到让sql.Rows实现了Rows接口的效果。
  2. 然后将Merge方法中的*sql.Rows替换为Rows —— Merge(ctx context.Context, group []Rows) (Rows, error) 使其更具通用性、组合性、扩展性。

批量查询实现问题

  1. Merger实现
type Merger struct{}

func (Merger) Merge(ctx context.Context, results []*sql.Rows) (merger.Rows, error) {
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}
	if len(results) == 0 {
		return nil, errs.ErrMergerEmptyRows
	}
	for i := 0; i < len(results); i++ {
                // 1. 为了使超时控制尽可能准确,每次循环前都要check一下ctx.Err()
                // 2. 对于sql.Rows类型的results[i]还要检查results[i].NextResultSet(), 有的sql.Rows中会包含多个结果集
                // Next()只在当前结果集中遍历,这里要考虑是否允许results[i]有多个结果集的情况,不允许就报错
		if results[i] == nil {
			return nil, errs.ErrMergerRowsIsNull
		}
	}
	return &MergerRows{
		rows: results,
		mu:   &sync.RWMutex{},
	}, nil
}
  1. MergerRows实现
type MergerRows struct {
	rows []*sql.Rows
	cnt  int
	mu   *sync.RWMutex
	once sync.Once
}

func (m *MergerRows) Next() bool {
	m.mu.RLock()
	if m.cnt >= len(m.rows) {
		m.mu.RUnlock()
		return false
	}
        // 与下方Scan并发会丢失行
	if m.rows[m.cnt].Next() {
		m.mu.RUnlock()
		return true
	}
	m.mu.RUnlock()
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.cnt >= len(m.rows) {
		return false
	}
	if m.rows[m.cnt].Next() {
		return true
	}
        // 如果支持多结果集,这里在更新m.cnt前需要调用NextResultSet()
	for {
		m.cnt++
		if m.cnt >= len(m.rows) {
			break
		}
		if m.rows[m.cnt].Next() {
			return true
		}
                // 如果支持多结果集,这里在更新m.cnt前需要调用NextResultSet()
	}
	return false

}

func (m *MergerRows) Scan(dest ...any) error {
	m.mu.RLock()
	defer m.mu.RUnlock()
        // 与上方Next中的m.rows[m.cnt].Next()会有并发问题
	return m.rows[m.cnt].Scan(dest...)
}

func (m *MergerRows) Close() error {
	var err error
	m.once.Do(func() {
		for i := 0; i < len(m.rows); i++ {
			row := m.rows[i]
			err = row.Close()
                        // 是否该保存一个err,然后向后继续遍历关闭下一个rows
			if err != nil {
				return
			}
		}
	})
	return err
}

func (m *MergerRows) Columns() ([]string, error) {
	return m.rows[0].Columns()
}
  • Scan()与Next()方法并发问题导致的丢失行问题,g1在Scan()方法的m.rows[m.cnt].Scan(dest...)处,g2执行到Next()的第一个if m.rows[m.cnt].Next() {...}把本该由g1读取走的缓存行用新行给覆盖掉了。如果MergerRows与sql.Rows语义一致即不能用于并发场景,那么读写锁m.mu的作用是什么?
  • 如果允许有多个结果集的情况,那么Next()方法中当m.rows[m.cnt].Next()返回false时,需要调用其上的NextResultSet()检查/获取下一个结果集
  • Close()方法,遇到第一个m.rows[i].Close()报错就返回似乎有些不妥,是否应该暂时记录下err,继续向后遍历关闭后续m.rows[i+1...N]。另外sql.Rows的Close是幂等的,所以多次调用也是安全的。
  • 缺少Err() 方法,merger.Rows的使用方法应该与sql.Rows一致,即在用for rows.Next() { rows.Scan(...) }之后用rows.Err()来检查迭代期间遇到的错误。

你的问题

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

重构:Update 语句忽略零值和Nil

仅限中文

使用场景

在 Update 语句中忽略零值或者 Nil 是一个非常常见的用法。这主要是因为如果一个字段是基础类型,例如 int 类型,那么当值是 0 的时候我们无法区别这个 0 是用户输入的 0 还是因为创建结构体实例的时候初始化的 0。

那么应该允许用户告诉我们 ORM 框架要不要忽略掉零值。

目前我们采用的设计是:

func AssignNotNilColumns(entity interface{}) []Assignable {
	// ...
}

// AssignNotZeroColumns uses the non-zero value to construct the Assignable instances.
func AssignNotZeroColumns(entity interface{}) []Assignable {
	// ...
}

这种是无侵入式地设计。

它的缺点也是明显的:

  • 不够直观,用户比较难想到使用这些方法
  • 性能比较差

另外一方面,我们在 INSERT 里面处理主键的时候,原本也是想采用这种无侵入式地设计,但是最终无法达成预期目标,所以使用的是 SkipPK 这种侵入式方案。

因此,为了保持风格一致,这一次我们将删掉这两个方法,并且增加新的方法:

// 只会忽略值为 nil 的
func (u *Updater) SkipNilValue

// 忽略零值的,那么实际上 nil 也会被忽略
func (u *Updater) SkipZeroValue

分库分表:NOT 支持

仅限中文

使用场景

NOT 的支持和 AND,OR 不太一样。对于 AND 和 OR 来说,目前可以预期的规则都是 AND 取交集,OR 取并集。NOT 看上去和 AND 和 OR 类似,但是实际上很不一样。

对于范围查询来说,假如说我们的分库分表规则是按照每100来分,那么 NOT(user_id >= 200) 等价于 (user_id <200)。这在范围查询里面是可以确定命中的表是 user_tab_0 和 user_tab_1 。

但是在哈希里面则不一样,比如说 %3 分库分表,那么 NOT(user_id = 1) 应该是广播。因为我们可以预期 user_id 可以是除了 1 以外的任何值,那么肯定是分散在所有的表里面。

因此 NOT 是一个和分库分表算法相关的东西。

Deleter

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Support DELETE query. In this phase, you don't need to support LIMIT clause.
(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

relies on #11

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

eorm: 利用 AOP 接口打印 SQL

仅限中文

使用场景

在日常开发或者 DEBUG 过程中,为了判断 ORM 框架是否执行如预期,那么会尝试将 SQL 输出。

但是在输出 SQL 的时候,一个核心的问题是要不要输出查询参数。

  • 不输出查询参数
  • 输出查询参数
  • 输出查询参数,但是一些敏感字段会用 *** 取代

另外一方面,考虑到不同的用户使用不同的日志组件(框架),所以我们打印 SQL 应该允许用户设置输出的目标位置。

正常来说,我们只能在开发或者测试环境里面打印 SQL,因为数据库查询是一个高频的,如果我们在线上也开启这个功能,那么对于日志组件来说,可能不堪重负。

而要在线上环境打印查询,那么就要考虑敏感字段的问题,例如用户的账号密码,邮箱或者手机号码等。这一类信息不能被打印出去,对于稍微不那么敏感的数据,经过 **** 掩码之后可以输出到日志里面,方便定位问题。

行业分析

就打印SQL而言,Beego ORM 和 GORM 都是采用的侵入式的方案,即在全局维护了一个 DEBUG 的标记位。如果设置了改标记位,那么就会打印 SQL,并且打印查询参数。总体来说,这种方案可行,但是不够优雅。

就我个人观察来说:

  • 用户很难知道需要设置这么一个标记位,对新手来说尤其如此
  • 用户也难以指定我这个 SQL 输出到哪里。所以类似这种中间件往往都会设计一个伴随的日志接口,或者日志模块。这也是我极力想要避免的

可行方案

基本上,我们只需要利用 AOP 接口提供一个实现就可以。参考 #114

该实现需要:

  • 允许用户指定输出的方式,默认情况下,输出到控制台
  • 允许用户指定是否输出参数,默认情况下不输出参数。用户可以自己考虑在测试环境开启输出参数。注意,这里默认不输出参数是为了避免有一些用户并不清楚数据加密的重要性,而导致无意间将线上敏感数据输出到了日志中

其它

  • 你需要提供单元测试

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

valuer/unsafe:删除对不同类型的特殊处理,直接依赖于 reflect.NewAt

仅限中文

使用场景

早期我在设计 eorm 处理结果集的部分的时候,最开始的想法是引入一个中间表达:

image

即我们最开始读取返回的查询结果集,我们使用 []byte 来接收,这个是一个中间表达。通过这样一个中间表达,我们可以在将结果集拼装成对象之前,做一些事情。

但是后来我发现这里面有两个缺点:

  • 我们需要自己处理编码问题,即如何将 []byte 转换成具体的类型。例如说有一个字段是 uint64,假如说值是13,那么我们收到的 []byte,如果使用的是 mysql 的 text protocol,那么拿到的是 "13" 的字符串,而在 mysql 启用了 prepare statement 之后,拿到的就是 13 对应的二进制编码
  • 性能损耗:在引入中间层之后会引入一些额外的内存分配和计算

所以最终我又去掉了这个中间表达,但是代码里面还有一些遗留代码。

这一次我们将去掉 valuer/unsafe 里面的相关代码:

func (u unsafeValue) Field(name string) (interface{}, error) {
	fd, ok := u.meta.FieldMap[name]
	if !ok {
		return nil, errs.NewInvalidFieldError(name)
	}
	ptr := unsafe.Pointer(uintptr(u.addr) + fd.Offset)
	if fd.IsHolderType {
		val := reflect.NewAt(fd.Typ, ptr).Elem()
		return val.Interface(), nil
	}
	switch fd.Typ.Kind() {
	case reflect.Bool:
		return *(*bool)(ptr), nil
	case reflect.Int:
		return *(*int)(ptr), nil
	case reflect.Int8:
		return *(*int8)(ptr), nil
	case reflect.Int16:
		return *(*int16)(ptr), nil
	case reflect.Int32:
		return *(*int32)(ptr), nil
	case reflect.Int64:
		return *(*int64)(ptr), nil
	case reflect.Uint:
		return *(*uint)(ptr), nil
	case reflect.Uint8:
		return *(*uint8)(ptr), nil
	case reflect.Uint16:
		return *(*uint16)(ptr), nil
	case reflect.Uint32:
		return *(*uint32)(ptr), nil
	case reflect.Uint64:
		return *(*uint64)(ptr), nil
	case reflect.Float32:
		return *(*float32)(ptr), nil
	case reflect.Float64:
		return *(*float64)(ptr), nil
	case reflect.String:
		return *(*string)(ptr), nil
	case reflect.Slice:
		// Array 只有一种可能,那就是 []byte
		return *(*[]byte)(ptr), nil
	case reflect.Pointer:
		ele := fd.Typ.Elem()
		switch ele.Kind() {
		case reflect.Bool:
			return *(**bool)(ptr), nil
		case reflect.Int:
			return *(**int)(ptr), nil
		case reflect.Int8:
			return *(**int8)(ptr), nil
		case reflect.Int16:
			return *(**int16)(ptr), nil
		case reflect.Int32:
			return *(**int32)(ptr), nil
		case reflect.Int64:
			return *(**int64)(ptr), nil
		case reflect.Uint:
			return *(**uint)(ptr), nil
		case reflect.Uint8:
			return *(**uint8)(ptr), nil
		case reflect.Uint16:
			return *(**uint16)(ptr), nil
		case reflect.Uint32:
			return *(**uint32)(ptr), nil
		case reflect.Uint64:
			return *(**uint64)(ptr), nil
		case reflect.Float32:
			return *(**float32)(ptr), nil
		case reflect.Float64:
			return *(**float64)(ptr), nil
		default:
			return nil, errs.NewUnsupportedTypeError(fd.Typ)
		}
	default:
		return nil, errs.NewUnsupportedTypeError(fd.Typ)
	}
}

这一大堆的 switch case,就是在引入中间结果 []byte 之后遗留产物,包括对 IsHolderType 的处理也是。

这个时候我们可以直接删除掉大部分代码:

func (u unsafeValue) Field(name string) (interface{}, error) {
	fd, ok := u.meta.FieldMap[name]
	if !ok {
		return nil, errs.NewInvalidFieldError(name)
	}
	ptr := unsafe.Pointer(uintptr(u.addr) + fd.Offset)
        val := reflect.NewAt(fd.Typ, ptr).Elem()
	return val.Interface(), nil
}

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Upserter for MySQL

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Support ON DUPLICATE KEY clause for MySQL dialect.

Although we define a function to generate upsert for PgSQL, you don't need to handle it now, let's support PgSQL in next phase.

(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

Selector

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

Support "SELECT" query. Now, we have already define APIs for Selector.

In this issue, you don't need to resolve WHERE clause (#11 ) and aggregate functions (#12 )

(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

github action: lint code and run tests

English Only

Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.

Use case(s)

We need a github action to force developers linting code and running tests before push instead of pre-commit.

And you could add more checkpoint if think it's neccessary.
(Please state use cases, what problem will this feature solve?)

Proposed Solution

(Please describe your proposed design/solution, if any)

Alternatives Considered

(Other possible options for solving the problem?)

Additional Context

(Paste any relevant logs - please use code blocks (```) to format console output,
logs, and code as it's very hard to read otherwise.)

(If you can, link to the line of code that might be helpful to explain the context)

分库分表:范围查询支持

仅限中文

背景

在最开始的时候,我们支持了 EQ 这种写法,AND 和 OR,并且在 #167 里面提及支持 NOT 写法。现在我们要进一步考虑剩余的操作符:

  • 比较操作符:>=, >, <, <=,
  • IN
  • BETWEEN

还有一个比较特殊的 !=,我会单独创建一个 issue,因为 != 可以看做是 NOT 的一种特殊场景。在 != 之下,就是广播的候选节点 - (= 确定的节点)。

很明显,我们这一次打算支持的就是范围查询。

使用场景

范围查询对于不同的分库分表规则来说目标表的确定是完全不同的。

哈希分库分表

假如说我们的 order 分库分表规则是 user_id%32 来分表。那么我们会面临这种困境,假如说分库分表的条件:

  • WHERE user_id < 10,那么很显然会命中 order_tab_[0, 9]
  • WHERE user_id <100,那么很显然是广播
  • 如果 user_id 有上限,比如说最大值是 1000,那么 WHERE user_id > 980 也只会命中部分节点
  • 如果 user_id 有上限,比如说最大值是1000,那么 WHERE user_id > 100 显然是广播

这种处理方案是有缺陷的,比如说:user_id > 1000 AND user_id < 10:那么先单一一个条件考察 user_id > 1000 会命中所有的表;user_id < 10 则只会命中全部集群上的部分库(db_[0,9])和部分表。那么根据 AND 的运算规则进行求交集,最终会选择第二个条件的所有的表。但是我们手动分析就知道,这个查询条件是查询不到任何数据的。

那么有些分库分表会引入一些SQL优化,就能一定程度上解决这种问题。

分库和分集群都是类似的。

范围分库分表

假如说我们的 order 分库分表规则是 user_id 每 1000 分一段。比如说 [0, 1000) 是一段,[1000,2000) 是一段...

  • WHERE user_id < 3000,那么很显然会命中 order_tab_[0, 2]
  • WHERE user_id >1000,那么很显然命中除了 order_tab_0 的全部节点。这里面我们可以注意到,在范围查询里面,在不知道上限的情况下,我们只能用排除法
  • WHERE user_id >1000,user_id 上限是 20000(不包含),那么就会命中 order_tab_[1, 19]

哈希分库分表

哈希分库分表规则在范围查询里面,几乎都是只能使用广播来进行处理。但是在一些边缘场景里面,存在一点优化的可能,但是实际上并没有必要去执行这种优化。

比如说对于 user_id %3 的分表,当查询条件是 user_id < 100 的时候,很显然是广播;但是如果查询条件是 user_id < 1,那么实际上我们可以确定只会命中 user_tab_0 这张表。

总结起来就是:对于哈希分库分表来说,在最大值和最小值附近,是可以不用广播的

不过这种优化价值实在不大,所以没有什么必要去做优化。

综合分库分表

假如说我们的分库分表规则是$region.mycompany.com:3306/order_db_(@user_id%32).order_tab_(@user_id/32%32)。那么本质上还是按照 AND,OR 来组合整个分集群分库分表。而对于具体的一条规则,则是参考范围分库分表和哈希分库分表来进行处理。

设计

在考虑支持范围查询的情况下,需要对已有的接口做一个较大的改造:

type Predicate = predicate.Predicate
type ShardingAlgorithm interface {
    Sharding(ctx context.Context, where Predicate) 
}

也就是我们需要把第二个参数改造成为 Predicate。在这种情况下,意味着我们要把 AND 和 OR 的逻辑也下沉到具体的算法实现里面。

理论上来说,我们可以考虑将 AND 和 OR 抽取出来作为公共方法,但是我在尝试的时候发现本身 AND 和 OR 是一个需要递归的东西,所以稍微有点棘手。

很显然这个 Predicate 就是我们在构造 WHERE 的时候把所有的查询条件 AND 成一个之后的产物。当然,这个地方也可以考虑将 where 定义为 []Predicate。

紧接着,我们在 internal 里面新建一个包,叫做 expression,然后将 eorm 包里面的 Predicate 结构体挪下去。同时在 eorm 里面定义一个同名的结构体 Predicate:

type Predicate = expression.Predicate

这里面比较麻烦的是在 eorm 本身里面将 Predicate 定义成为了一个 binaryExpression 的衍生类型,所以在重构的时候,需要将所有的 expression 实现都挪下去。同时,在 eorm 里面定义对应的别名。

这边我认为是可以的,因为 expression 不依赖于别的东西。

而后在具体的实现里面完成分库分表的逻辑。

eorm: Update 的执行

仅限中文

使用场景

正如在 #92 中提及,Deleter 设计出来之后只解决了构造 SQL 的过程,但是尚未实现发起查询。

你需要:

  • 参考 Inserter 的实现,发起查询
  • 提供基于 sqlmock 的单元测试
  • 提供集成测试

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

eorm: Distinct 关键字支持

仅限中文

使用场景

使用 Distinct 关键字主要有两个地方:

  • SELECT DISTINCT xxx, xx FROM
  • SELECT COUNT(DISTINCT) :这种形态可以称之为在聚合函数中使用 Distinct 关键字

第一种形态最为常见,而第二种形态则不那么常见,也是我们设计和实现的一个难点。

行业分析

实际上,在 Distinct 支持上有几种风格:

  • 直接 db.Distinct(xxx) 这种,即 xxx 代表的是一些列,往往都是字符串。GORM 就是这种风格
  • 只是简单拼接一个 DISTINCT 关键字,Beego ORM 中的 QueryBuilder 就是这种风格

而对于第二种形态来说,大多数 ORM 框架都是在解决 Select xxx 列的时候一并解决的。普遍来说,他们的方案是:

db.Select("COUNT(DISTINTCT id)")

也就是用字符串来作为输入,那么在这种情况下,用户可以输入任意的东西,自然也就包含了带 Distinct 关键字的聚合函数。

可行方案

按照行业惯例和我们已有的设计,第一种形态我们可以直接添加一个新的 Distinct 方法:

func (s *Selector) Distinct()  *Selector {

}

执行了这个调用之后生成的 Select 语句,会带上 DISTINCT 关键字:SELECT DISTINCT xxx...

而对于第二种形态来说,我们有两种选择:
第一种, 什么也不做。因为目前用户可以直接使用我们的 RawExpr 来达成目标:`s.Select(Raw("COUNT(DISTINCT id)"))

第二种,我们考虑帮助用户解决问题,那么也有两条路:

// 我们自己内部利用 RawExpr 来封装一下。在这里 col 必须是数据库内部的列名
func CountDistinct(col...string) RawExpr {
    return Raw("COUNT DISTINCT cols....")
}

// 这种设计形态下, col 实际上是字段名,同时我们可以对 col 进行校验,确保用户不会输错。
func CountDistinct(col string) Aggregate {
     return Aggregate{
              fn: "COUNT",
              arg: col,
              distinct: true // 这是在 Aggregate 中新加的字段。这一类的 Aggregate 才会设置为 true
     }
}

当我们要做的时候,就需要对 Count, Avg, Sum 都提供一个对应的方法,而 Max 和 Min 则不需要。

其它

你需要验证一个东西,能不能使用类似的 SELECT 语句:

SELECT * FROM xx HAVING COUNT(DISTINCT id) > 10

即在 HAVING 子句中使用这种东西。

此外:

  • 你需要设计单元测试和集成测试
  • 如果可以使用在 HAVING 子句中,那么集成测试要包含该用例

你使用的是 eorm 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.