返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

prepare

发布于 2024-10-12 19:15:52 字数 4575 浏览 0 评论 0 收藏 0

所谓 prepare statment ,是指数据库对 SQL 进行预处理(检查、优化、编译等)。

优点:

  • 占位符加参数方式更方便,可防止 SQL 注入等问题。
  • 某些数据库使用二进制协议。相比文本,效率更高。
  • 对于多次执行,消除了解析等开销,性能更好。

缺点:

  • 首次预处理时间较长。
  • 多次网络通讯开销( prepare / exec / close )。

分客户端和服务器两种实现方式。
占位符MySQL?PostgreSQL$1 $2SQLite ? OR $1

  • 某些驱动,将含参数操作一律当 Prepare 执行。
  • 不含参数的 Simple Exec ,可能有更好的性能。
  • 应及时调用 Stmt.Close 释放资源。
package main

import (
    "database/sql"
    "fmt"
    "log"
    
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    log.SetFlags(log.Lshortfile)
    
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil { log.Fatalln(err) }
    defer db.Close()
    
    // ------------------
    
    q := `INSERT INTO user (id, name) VALUES (?, ?)`
    
    stmt, err := db.Prepare(q)
    if err != nil { log.Fatalln(err) }
        
    for i := 0; i < 10; i++ {
        result, err := stmt.Exec(i, fmt.Sprintf("u%d", i))
        if err != nil { log.Fatalln(err) }
        
        fmt.Println(result.RowsAffected())
    }    
    
    // 关闭,并释放资源!
    if err := stmt.Close(); err != nil {
        log.Fatalln(err)
    }
}

源码剖析

虽然 Prepare 退出前调用 releaseConn 归还了连接,但 Stmt.css 依旧持有该连接引用。
因为 Prepare 总是和特定连接相绑定。

// database/sql/sql.go

func (db *DB) prepare(ctx, query, strategy) (*Stmt, error) {
	dc, err := db.conn(ctx, strategy)
	return db.prepareDC(ctx, dc, dc.releaseConn, nil, query)
}
func (db *DB) prepareDC(ctx, dc, release, cg, query) (*Stmt, error) {
    
    // 释放连接。
	defer func() {
		release(err)
	}()
    
    // 执行!
	withLock(dc, func() {
		ds, err = dc.prepareLocked(ctx, cg, query)
	})
	if err != nil {
		return nil, err
	}
    
	stmt := &Stmt{
		db:    db,
		query: query,
		cg:    cg,
		cgds:  ds,
	}

    // 继续持有连接引用!
    // cg != nil --> transaction!!!
	if cg == nil {
		stmt.css = []connStmt{{dc, ds}}
		db.addDep(stmt, stmt)
	}
    
	return stmt, nil
}

当执行 Stmt.ExecQuery 等操作时,会尝试从连接池中获取 “记忆” 连接。
获取失败,则在新连接上重新执行 Prepare 操作。新连接也会被释放。

func (s *Stmt) ExecContext(ctx context.Context, args ...any) (Result, error) {
	for i := 0; i < maxBadConnRetries+1; i++ {
        
        // 找到记忆连接,或新建。
		dc, releaseConn, ds, err := s.connStmt(ctx, strategy)
        
        ...
        
        // 执行,释放连接。
		res, err = resultFromStatement(ctx, dc.ci, ds, args...)
		releaseConn(err)
        
		if !errors.Is(err, driver.ErrBadConn) {
			return res, err
		}
	}
    
	return nil, driver.ErrBadConn
}
func (s *Stmt) connStmt(ctx, strategy) (dc, releaseConn, ds, err error) {

    // 从池取一个连接。
	dc, err = s.db.conn(ctx, strategy)
	if err != nil {
		return nil, nil, nil, err
	}

    // 判断是否记忆连接。
	for _, v := range s.css {
		if v.dc == dc {
			return dc, dc.releaseConn, v.ds, nil
		}
	}

    // 重新执行 Prepare,并记下新连接。
	withLock(dc, func() {
		ds, err = s.prepareOnConnLocked(ctx, dc)
	})

	return dc, dc.releaseConn, ds, nil
}

清理

执行完毕,如何清理 Stmt.css 资源?毕竟数据库保存了这些连接的 Prepare 状态。
首先,在 prepareDC 尾部有 db.addDep(stmt, stmt) 调用。

func (db *DB) addDepLocked(x finalCloser, dep any) {
	if db.dep == nil {
		db.dep = make(map[finalCloser]depSet)
	}
    
	xdep := db.dep[x]
	if xdep == nil {
		xdep = make(depSet)
		db.dep[x] = xdep
	}
    
	xdep[dep] = true
}

Stmt.Close 被调用,会移除这个设置,并调用 finalCloser

func (s *Stmt) Close() error {
	if s.cg == nil {
		return s.db.removeDep(s, s)
	}
    
	return txds.Close()
}
func (db *DB) removeDep(x finalCloser, dep any) error {
	fn := db.removeDepLocked(x, dep)
	return fn()
}

func (db *DB) removeDepLocked(x finalCloser, dep any) func() error {
	switch len(xdep) {
	case 0:
		// No more dependencies.
		delete(db.dep, x)
		return x.finalClose
	}
}

很显然,这对应 Stmt.finalClose 方法,通知驱动程序关闭相关资源。

func (s *Stmt) finalClose() error {
    
	if s.css != nil {
		for _, v := range s.css {
			s.db.noteUnusedDriverStatement(v.dc, v.ds)
			v.dc.removeOpenStmt(v.ds)
		}
		s.css = nil
	}
    
	return nil
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文