ecodeclub / eorm

简单 ORM 框架
Apache License 2.0
191 stars 64 forks source link

重构:将分库分表的增删改查修改为 DB 维度执行。 #212

Closed flycash closed 6 months ago

flycash commented 1 year ago

一个令人悲伤的事情是,Go sql driver 有一个 BUG,https://github.com/go-sql-driver/mysql/issues/314 。 这个 BUG 导致这么一个问题:如果你要在数据事务里面,发起查询之后,没有 close 掉结果集,那么再次发起查询的时候,就会报 buzy buffer 的错误

由事务衍生出来的 PrepareStatement 同样如此。

例如利用集成测试的数据库,运行这个测试就可以复现:

func TestDBTx(t *testing.T) {
    db, err := sql.Open("mysql", "root:root@tcp(localhost:13307)/order_detail_db_1")
    if err != nil {
        t.Fatal(err)
    }
    tx, err := db.Begin()
    if err != nil {
        t.Fatal(err)
    }
    rows1, err := tx.Query("SELECT * FROM order_detail_tab_0 LIMIT 0, 10")
    if err != nil {
        t.Fatal(err)
    }

    rows2, err := tx.Query("SELECT * FROM order_detail_tab_1 LIMIT 0, 10")
    if err != nil {
        t.Fatal(err)
    }

    t.Log(rows1, rows2)
}

错误信息是:

=== RUN   TestDBTx
[mysql] 2023/06/29 19:40:38 packets.go:447: busy buffer
    delay_transaction_test.go:298: driver: bad connection
--- FAIL: TestDBTx (0.03s)

FAIL

Debugger finished with the exit code 0

在 sql SDK 不打算解决这个 BUG 的情况下,我们有两个可选的方案:

修改已有的结果集处理这个会引入非常严重的问题, 所以几乎不能选。

第二种方案是我之前就提到过的,它本身就属于性能优化的一点。因此现在我们需要做:

为了支持这种特性,用户必须指定 multiStatements 参数为 true。这就带来另外一层麻烦:

但是可以预见的是,multiStatements=true 肯定会带来很多困扰,也就是很多用户搞不明白为啥一定要有 multiStatements=true,要么是没读文档,要么是没读明白。

flycash commented 1 year ago

所谓改为 DB 维度执行,就是现在我们生成的 Query 都是表维度的单一的 SQL,比如说:

qs := []Query{
     Query{
           SQL: "SELECT * FROM db_0.tab0 WHER ..."
     },
     Query{
           SQL: "SELECT * FROM db_0.tab1 WHER ..."
     },
}

而修改为 DB 维度之后,我们的目标 SQL 就变成了:

qs := []Query{
     Query{
           SQL: "SELECT * FROM db_0.tab0 WHER ...;SELECT * FROM db_0.tab1 WHER ..."
     }
}
Stone-afk commented 1 year ago
// DB represents a database
type DB struct {
    core
    ds datasource.DataSource

    multiStatements   bool
    interpolateParams bool
}

func DBWithMultiStatements(m bool) DBOption {
    return func(db *DB) {
        db.multiStatements = m
    }
}

func DBWithInterpolateParams(p bool) DBOption {
    return func(db *DB) {
        db.interpolateParams = p
    }
}

func Open(driver string, dsn string, opts ...DBOption) (*DB, error) {
    dl, err := dialect.Of(driver)
    if err != nil {
        return nil, err
    }
    orm := &DB{
        core: core{
            metaRegistry: model.NewMetaRegistry(),
            dialect:      dl,
            // 可以设为默认,因为原本这里也有默认
            valCreator: valuer.PrimitiveCreator{
                Creator: valuer.NewUnsafeValue,
            },
        },
    }
    for _, o := range opts {
        o(orm)
    }

    db, err := single.OpenDB(driver, dsn,
        single.DBWithMultiStatements(orm.multiStatements),
        single.DBWithInterpolateParams(orm.interpolateParams))
    if err != nil {
        return nil, err
    }
    orm.ds = db

    return orm, nil
}

在最外层的 db 里需要指定这个参数,然后 在 single db 里还要再指定一次,通过最外层的DB 传给 single db

Stone-afk commented 1 year ago

这里感觉设计和写法很差,还有其他的方案吗,如果是直接传参那更差了

Stone-afk commented 1 year ago
// DB represents a database
type DB struct {
    db *sql.DB

    multiStatements   bool
    interpolateParams bool
}

type Option func(db *DB)

func DBWithMultiStatements(m bool) Option {
    return func(db *DB) {
        db.multiStatements = m
    }
}

func DBWithInterpolateParams(p bool) Option {
    return func(db *DB) {
        db.interpolateParams = p
    }
}

func OpenDB(driver string, dsn string, opts ...Option) (*DB, error) {

    res := &DB{}

    for _, opt := range opts {
        opt(res)
    }

    if res.multiStatements {
        dsn = dsn + "?multiStatements=true"
    }

    if res.interpolateParams {
        dsn = dsn + "&interpolateParams=true"
    }

    db, err := sql.Open(driver, dsn)
    if err != nil {
        return nil, err
    }
    res.db = db
    return res, nil
}
Stone-afk commented 1 year ago

根据文档,multiStatements 要和 interpolateParams 参数一起用,多条sql语句才生效,明佬可以再次求证一下

flycash commented 1 year ago

OKKK,我现在担心的就是,怎么跟用户解释这个事情=。=

Stone-afk commented 1 year ago

mark

flycash commented 1 year ago

OpenDB 里面不要帮用户拼接,因为问题就是你拼接了,但是大部分用户是不看源码的,所以他不懂。然后出问题了他就会很奇怪。 所以 OpenDB 里面就检测一下有没有 dsn 有咩有传入 multiplestatmes 和 interpolate 这两个参数。

if !strings.contains("multiStatements=true") {
    return errors.New("分库分表下 dsn 必须传入 multiStatements=true 参数")
}