ecodeclub / eorm Goto Github PK
View Code? Open in Web Editor NEW简单 ORM 框架
License: Apache License 2.0
简单 ORM 框架
License: Apache License 2.0
仅限中文
这也算是一个实验性质的方案。目前我们有一个 Session 的抽象,那么我们可以考虑提供一个 MasterSlavesDB 实现来支持读写分离。
基本设计就是:
type MasterSlavesDB struct {
master *sql.DB
slaves *sql.DB
}
对于 SELECT 语句来说,默认情况下使用从库,其它语句默认使用主库。
但是有一个特殊的场景需要考虑,那就是在开启了事务的情况下,所有的查询都应该使用主库。那么相应的,我们需要修改已有的 Tx 结构体(或者提供一个新的实现),以保持这种语义。
另外还应该允许用户强制指定使用主库,但是不需要考虑用户要求强制使用某个从库的情况。
到目前为止,你不需要考虑从库负载均衡的问题,采用默认的轮询就可以,我们后面再考虑引入不同的负载均衡设计。
仅限中文
目前在合并请求 #145 里面我们初步解决了最简单的分库分表场景,即只考虑等值查询的条件,该如何生成SQL。
现在我们需要进一步强化 “分库分表” 规则这么一个概念。对于一个分库分表规则来说,它需要解决:
进一步说,如果在提供了分库分表键的情况下我们只能确定库,那么分库分表规则能够返回该库下面的所有的命中的表,也就是第一种情况的一个特例而已。
但是进一步思考,我们会发现这个 ShardingAlgorithm 不是很好设计。因为用户需要的各种分库分表规则简直千变万化:
很多时候,分库分表并不是只使用我们前面提到的那些基本分库分表的做法,而是可能涉及到多个组合在一起。
最典型的就是在压测的情况下,同时业务数据本身就是分库分表的。举个例子,假如说现在我们的生产库 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 内部分库分表的规则又不同,比如说**大陆人口众多,那么按照 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(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
}
注意:
那么用伪代码来描述使用起来的效果则是:
meta := db.r.Get(t)
shardingResp := meta.algorithm.Sharding(req)
for _, dst := range shardingResp.Dsts {
ds := findDs(dst.Name)
ds.Exec(ctx, sql, args...)
}
用文字来简短描述则是:
我计划支持一个统一的 Datasource 抽象。它会有以下实现:
目前在第一期里面,这个不需要支持,后续再支持,我会额外创建 issue。
在引用了这个抽象之后,对 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。
后续考虑支持,第一期不支持。
单元测试必须覆盖以下所有的场景:
以上所有的测试用例要进一步考虑 AND,OR,NOT 的效果
在一些特定情况下,用户对一张表都有多套分库分表,这种我们暂时不打算支持。我总体上认为是业务层面上设计不合理,所以中间件犯不着支持,毕竟我们是开源又不是公司内部项目,老板说支持就必须支持。
而且大多数情况下,用户可以通过组合 Algorithm 来达成类似的目标。
正如我之前说过的,我们并不打算解决所有的问题。我们这里主要集中解决增删改查,而且是对业务数据的增删改查。
那么一些特别的语句,比如说 CREATE USER 之类的。当然并不是说不能执行,而是说犯不着特意在分库分表内核为它们留下接口。
只是说随缘,如果不需要额外的努力也恰好可以支持,那就支持;如果需要额外的努力,那么我们就不支持。
前面梳理了不同的分库分表场景之后,我要强烈批评两个错误设计:
就第一个问题来说,我认为很多分库分表中间件都犯了这个错误。比如说对哈希分库分表进行了特殊处理,或者说给予了特殊的地位。甚至于在设计表达式的时候,都给予了它特殊的地位。
这里我要提及一个案例,就是某司的分库分表中间件,欠缺一个对分库分表算法的一个抽象。后来果然在扩展支持一些功能的时候遇到了问题。比如说另我印象很深刻的就是支持 zone 概念的时候,在分库分表中间件中引入了和 zone 相关的很多概念。
他们的做法不同于我在这里提及的把 zone 等概念限定到某一个具体的分库分表算法的实现里面,而是在分库分表核心内部就引入了很多和 zone 相关的东西。
这就是典型的巨大的设计错误。而且在可以预计的未来,他们还会遇到更多的困难。
当然这并不是这家公司这部分开发人员才会犯下的错误,而是大多数设计者在设计中间件都会犯的错误。
常见的原因则是中间件研发者往往会受到业务研发的影响。一个业务研发认为重要的功能,中间件研发者很容易被误导,做成框架的核心功能。
而实际上,业务上认为重要的功能并不等于中间件要解决的核心问题。那么对于一个研发者来说,他就要审慎思考当公司要求中间件提供一个功能的时候,这个功能究竟是不是中间件的核心功能。
仅限中文
在 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
。
我对 Field
和 FieldByName
两个做了性能测试,效果如下:
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 设置好。
要求:
Index
, ColumnMeta 在internal/model 包中reflectValue
实现,将 FieldByName
替换为 Field
仅限中文
在有一些场景下我们会在 SELECT 里面使用聚合函数,例如:
SELECT AVG(`age`) FROM users
在这种情况下,我们会希望 Selector 能够返回基本类型:
avgAge := NewSelector[int].Select(xxx).From(&User{}).Where(xxx).Get(ctx)
相比我们已经支持的用法,它的改动主要涉及到:
除了 Get,显然还需要支持 MultiGet。
而要支持的类型包括:
在 Go 里面基本没有什么可以参考的东西,因为这是第一次采用泛型来解决聚合函数的问题。
实际上,如果我们不考虑已有的 valuer 抽象的话,实现起来还是很容易的,我们只需要在 Get 和 MultiGet 里面进行类型判断,而后直接调用 Scan 方法,这也就是最简单的方法。
我大概评估了一下,如果我们希望将这个逻辑下沉到 valuer.SetFields 方法里面,那么还是比较棘手的。这种做法的好处是保持住了 valuer 抽象。
上传
go env
的结果
仅限中文
因为在分库分表的 ShardingSelector 里面我们需要用到 MasterSlavesDB,所以我先合并了合并请求,但是针对 MasterSlavesDB 的单元测试不够完善,所以我们还需要进一步提供单元测试。
为了支持单元测试,我们可以考虑使用 mock 的 Slaves 实现。这样可以确保我们一定走过去了主节点或者从节点。
仅限中文
项目缺少格式化工具,详见 #82
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
仅限中文
在 GO sql 包里面有两个很特殊的接口:sql.Scanner 和 driver.Valuer。前者用于接收数据库返回的数据,后者用于返回查询的参数。典型的使用场景就是 sql.NullXXX 这些类型。
那么实现了这两个接口的类型就不能被当做普通的类型处理。因此在 ColumnMeta 里面设置了一个标记位 IsHolderType,目前来看这个标记位有点多余,所以可以删掉。
核心还是在于我简化了整体实现,所以已经用不上了。
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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.
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
(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)
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
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)
在前面的 #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 有关的概念了。
可以定义为:
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,还是一个主从结构,还是说混合了影子库影子表的东西。
对于主从数据源来说,可以定义为:
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 就是一个 Datasource 实例,而后多个 zone 对应的 Datasource 可以合并在一起构成一个业务上有意义的单位。
比如说订单表,那么可能有** zone,可能有美国 zone,那么分库分表无非就是要确认某一个请求是发给美国 zone 还是发给** zone。
当然,如果是彻底的隔离机制,那么我们可以做到说,在初始化 zone 所代表的 Datasource时,我们只初始化所在 zone 的那个实例。
而不管如何,这些事情都可以被限制在 Datasource 的某一个实现之类。依旧可以确保说整个分库分表的核心是和 zone 没有什么关系的。
不得不说,这是我借鉴得非常失败的一个概念。本身源自某 ORM 框架的概念,但是现在我看来,Session 应该被 Datasource 取代。
之前是顾虑到有一类场景,用户可能希望把一些的操作放在一个上下文内,在这个上下文内他们可以考虑设置一些共享的设置,比如说链接参数、ORM 的 middleware 等。
但是现在看起来,完全没有这个必要。
接下来我们还会保留这个 Session 概念,但是会进一步淡化它的作用。
仅限中文
很显然,到目前为止我们只支持了 SELECT 和 INSERT,在 DELETE 语句上,我们只支持了构造出来 DELETE 语句,但是没有支持发起 DELETE 查询并且返回结果。
DELETE 语句和 INSERT 语句在执行上并没有什么差距,所以可以直接将 Inserter 的执行代码复制一份过来。
但是你需要提供:
仅限中文
正常来说,在一家大规模的公司里面,我们不太可能直接指定所有的 slaves,因为从库可能都有数十个。
具体来说:
第二种我们不需要管,但是我们需要为第三种提供一个实现。这个实现的功能非常清晰:
如果考虑到容错,那么需要:
整个过程,类似于基于 DNS 的服务发现和负载均衡。
容错是一个非常复杂的话题,你可以创建一个草稿合并请求,我们在那个合并请求里面具体讨论。
仅限中文
之前在尝试支持 HAVING 的时候,我支持了这种语法形态:
select AVG(age) as avg_age from xxx group by xx 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{}
}
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
We need some structures to represent the table and column:
type TableMeta struct {
}
type ColumnMeta struct {
}
TableMeta
at least contains those information:
ColumnMeta
at least contains those information:
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)
(Other possible options for solving the problem?)
(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)
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
(Please state use cases, what problem will this feature solve?)
Predicates are used to filter rows. Predicate
can be used in WHERE clause.
(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
(Other possible options for solving the problem?)
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)
仅限中文
早期在考虑支持增删改查的时候,我认为允许用户指定主键是一个很合理的事情,因为我们可以考虑支持诸如 UpdateByPK 之类的方法。
但是目前来说,我成功规避了所有类似的设计,因为本身我在维护 Beego ORM,或者审视 GORM 的设计的时候,我认为 ORM 框架完全不应该关心一个列是不是主键。
那么类似于 UpdateByPK 这种调用,用户可以直接在 Where 方法里面指定 C("Id").EQ(123)。
这有两种效果:
所以我现在希望删除在 ColumnMeta 里面的 IsAutoIncrement 字段,而后修改对应的代码和测试用例。
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
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)
(Other possible options for solving the problem?)
(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, 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 字段。
上传
go env
的结果
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
(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.
(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.
(Other possible options for solving the problem?)
(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)
仅限中文
在搜索列的时候,我们会考虑检索字符串是否符合某个特征,在这种情况下,我们可以考虑使用 LIKE 查询。
例如:
SELECT * FROM XXX WHERE XXX LIKE '%abc'
LIKE 查询在很多中间件里面都提供了支持,并且它们还进一步封装了:
提供一个 LIKE 调用,接近于 #101 #102 中的设计:
C("name").Like("abc")
C("name").NotLike("abc")
这里 Like 和 NotLike 接收字符串类型。另外就是我们也不对 name 的类型进行校验,例如即便用户使用的是 int 类型,他要用 Like 也是可以的。
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
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.
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:
unsafe
and reflection
(State a summary of your enhancement suggestion)
(Get your go version by go version
)
(If you can, link to the line of code that could be improved)
(What you think should be enhanced and how?)
仅限中文
仅限中文
这个作为初步实现,用来验证我们的设计。根据我们讨论的内容,ShardingSelector 牵涉到:
所以很显然:
依赖于 #140
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
"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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
(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)
仅限中文
在插入的时候,如果用户主键使用的是基本类型,那么我们依旧会插入基本类型的零值,例如:
type User struct {
Id uint64
}
NewInserter(db).Values(&User{})... // 会插入主键为 0 的 user
而实际上用户是希望忽略主键的。
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
核心是要告诉 Inserter 忽略主键。从理论上来说有两种思路:
从我个人的使用偏好上来说,我更加喜欢第一个方案,但是可能用户更加喜欢第二个选项
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
仅限中文
我们将启动新的分库分表特性支持。
为了支持分库分表,一个重要的抽象就是处理结果集,这个抽象被我命名为:
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 方法。
仅限中文
在 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 (?, ?, ?)
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
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)
仅限中文
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
Define two functions:
func (p Predicate) In(val interface{})
func (p Predicate) NotIn(val interface{})
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
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)
仅限中文
由讨论 #82 和 #84 启发而来,我们需要检查代码 linter 问题。
目前我们现在已经集成了 deepsource 和 codecov 两个 action,从理论上来说它们都支持静态代码质量检测。但是我并不确定它们是否有缺漏。
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
目前市面上做静态代码检测的工具有很多:
如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择
事实上,在我们的 make setup 命令里面已经把 golangci-lint 放到了 pre-push 里面,所以也就是如果贡献者按照标准初始化了开发环境,那么他们推送代码的时候就会触发。
但是我们依旧需要一个 github action 来做检测,以确保我们的贡献者确实做了这些检查。
要额外开启 [asasalint](https://github.com/alingse/asasalint)
任何你觉得有利于解决问题的补充说明
任何需要补充额外开启的 linter 可以在这里补充
上传
go env
的结果
仅限中文
我在用的时候我才发下,我居然没有实现 GetMulti 方法。
func (s *Selector[T]) GetMulti(ctx context.Context) ([]*T, error) {
}
实现这个方法可以参考 Selector 上的 Get 方法,并且核心逻辑,也就是处理结果集的地方,是可以复用的。
有一个地方不太一样,即 GetMulti 本身不会返回 ErrNoRows 的错误,因为我们认为在批量查询的时候,没有结果是一个很正常的事情。这也是为了和 sql 包的语义保持一致。
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
仅限中文
在处理结果集的时候,排序是一种常见的情况。例如逻辑上的 SQL 是 SELECT * FROM user_tab WHERE id
> 100 ORDER BY id ASC
那么基本上会将请求发送到非常多个表上。在处理结果集的时候,需要将这些返回的全部结果再进行一次排序。
从实现角度来说:
注意,用户必须指定排序的列,如果没有指定则是报错。这意味着我们在这层面上并不会默认使用主键来进行排序。
仅限中文
我在极客时间的实战训练营里面已经讲得很清楚了。设计文档后续补,TODO
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
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)
(Other possible options for solving the problem?)
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)
当前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)
}
Merge(ctx context.Context, group []Rows) (Rows, error)
使其更具通用性、组合性、扩展性。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
}
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()
}
m.rows[m.cnt].Scan(dest...)
处,g2执行到Next()的第一个if m.rows[m.cnt].Next() {...}
把本该由g1读取走的缓存行用新行给覆盖掉了。如果MergerRows与sql.Rows语义一致即不能用于并发场景,那么读写锁m.mu
的作用是什么?m.rows[m.cnt].Next()
返回false时,需要调用其上的NextResultSet()
检查/获取下一个结果集for rows.Next() { rows.Scan(...) }
之后用rows.Err()
来检查迭代期间遇到的错误。上传
go env
的结果
仅限中文
在 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 的支持和 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 是一个和分库分表算法相关的东西。
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
Support DELETE
query. In this phase, you don't need to support LIMIT
clause.
(Please state use cases, what problem will this feature solve?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
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)
仅限中文
在日常开发或者 DEBUG 过程中,为了判断 ORM 框架是否执行如预期,那么会尝试将 SQL 输出。
但是在输出 SQL 的时候,一个核心的问题是要不要输出查询参数。
另外一方面,考虑到不同的用户使用不同的日志组件(框架),所以我们打印 SQL 应该允许用户设置输出的目标位置。
正常来说,我们只能在开发或者测试环境里面打印 SQL,因为数据库查询是一个高频的,如果我们在线上也开启这个功能,那么对于日志组件来说,可能不堪重负。
而要在线上环境打印查询,那么就要考虑敏感字段的问题,例如用户的账号密码,邮箱或者手机号码等。这一类信息不能被打印出去,对于稍微不那么敏感的数据,经过 **** 掩码之后可以输出到日志里面,方便定位问题。
就打印SQL而言,Beego ORM 和 GORM 都是采用的侵入式的方案,即在全局维护了一个 DEBUG 的标记位。如果设置了改标记位,那么就会打印 SQL,并且打印查询参数。总体来说,这种方案可行,但是不够优雅。
就我个人观察来说:
基本上,我们只需要利用 AOP 接口提供一个实现就可以。参考 #114 。
该实现需要:
上传
go env
的结果
仅限中文
早期我在设计 eorm 处理结果集的部分的时候,最开始的想法是引入一个中间表达:
即我们最开始读取返回的查询结果集,我们使用 []byte 来接收,这个是一个中间表达。通过这样一个中间表达,我们可以在将结果集拼装成对象之前,做一些事情。
但是后来我发现这里面有两个缺点:
所以最终我又去掉了这个中间表达,但是代码里面还有一些遗留代码。
这一次我们将去掉 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
}
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
(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)
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
(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)
Please check existing issues first to avoid duplication and answer the questions below before submitting your issue.
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?)
(Please describe your proposed design/solution, if any)
(Other possible options for solving the problem?)
(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 写法。现在我们要进一步考虑剩余的操作符:
还有一个比较特殊的 !=,我会单独创建一个 issue,因为 != 可以看做是 NOT 的一种特殊场景。在 != 之下,就是广播的候选节点 - (= 确定的节点)。
很明显,我们这一次打算支持的就是范围查询。
范围查询对于不同的分库分表规则来说目标表的确定是完全不同的。
假如说我们的 order 分库分表规则是 user_id%32 来分表。那么我们会面临这种困境,假如说分库分表的条件:
这种处理方案是有缺陷的,比如说: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) 是一段...
哈希分库分表规则在范围查询里面,几乎都是只能使用广播来进行处理。但是在一些边缘场景里面,存在一点优化的可能,但是实际上并没有必要去执行这种优化。
比如说对于 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 不依赖于别的东西。
而后在具体的实现里面完成分库分表的逻辑。
仅限中文
正如在 #92 中提及,Deleter 设计出来之后只解决了构造 SQL 的过程,但是尚未实现发起查询。
你需要:
如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子
如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择
任何你觉得有利于解决问题的补充说明
上传
go env
的结果
仅限中文
使用 Distinct 关键字主要有两个地方:
第一种形态最为常见,而第二种形态则不那么常见,也是我们设计和实现的一个难点。
实际上,在 Distinct 支持上有几种风格:
而对于第二种形态来说,大多数 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 子句中使用这种东西。
此外:
上传
go env
的结果
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.