Gorm 源码分析(一) database/sql

栏目: 数据库 · 发布时间: 6年前

内容简介:Gorm是Go语言开发用的比较多的一个ORM。它的功能比较全:但是这篇文章中并不会直接看Gorm的源码,我们会先从database/sql分析。原因是Gorm也是基于这个包来封装的一些功能。所以只有先了解了database/sql包才能更加好的理解Gorm源码。database/sql 其实也是一个对于mysql驱动的上层封装。"github.com/go-sql-driver/mysql"就是一个对于mysql的驱动,database/sql 就是在这个基础上做的基本封装包含连接池的使用

简介

Gorm是 Go 语言开发用的比较多的一个ORM。它的功能比较全:

  • 增删改查
  • 关联(包含一个,包含多个,属于,多对多,多种包含)
  • CallBacks(创建、保存、更新、删除、查询找)之前 之后都可以有callback函数
  • 预加载
  • 事务
  • 复合主键
  • 日志

database/sql 包

但是这篇文章中并不会直接看Gorm的源码,我们会先从database/sql分析。原因是Gorm也是基于这个包来封装的一些功能。所以只有先了解了database/sql包才能更加好的理解Gorm源码。

database/sql 其实也是一个对于 mysql 驱动的上层封装。"github.com/go-sql-driver/mysql"就是一个对于mysql的驱动,database/sql 就是在这个基础上做的基本封装包含连接池的使用

使用例子

下面这个是最基本的增删改查操作

操作分下面几个步骤:

  1. 引入github.com/go-sql-driver/mysql包(包中的init方法会初始化mysql驱动的注册)
  2. 使用sql.Open 初始化一个sql.DB结构
  3. 调用Prepare Exec 执行 sql 语句

==注意:==使用Exec函数无需释放调用完毕之后会自动释放,把连接放入连接池中

使用Query 返回的sql.rows 需要手动释放连接 rows.Close()
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "strconv"
)

func main() {
    // 打开连接
    db, err := sql.Open("mysql", "root:feg@125800@tcp(47.100.245.167:3306)/artifact?charset=utf8&loc=Asia%2FShanghai&parseTime=True")
    if err != nil {
        fmt.Println("err:", err)
    }
    // 设置最大空闲连接数
    db.SetMaxIdleConns(1)
    // 设置最大链接数
    db.SetMaxOpenConns(1)
    query(db, 3)
}

//修改
func update(db *sql.DB, id int, user string) {
    stmt, err := db.Prepare("update user set UserName=? where Id =?")
    if err != nil {
        fmt.Println(err)
    }
    res, err := stmt.Exec(user, id)
    updateId, err := res.LastInsertId()
    fmt.Println(updateId)
}

//删除
func delete(db *sql.DB, id int) {
    stmt, err := db.Prepare("delete  from user where id = ?")
    if err != nil {
        fmt.Println(err)
    }
    res, err := stmt.Exec(1)
    updateId, err := res.LastInsertId()
    fmt.Println(updateId)
}

//查询
func query(db *sql.DB, id int) {
    rows, err := db.Query("select * from user where  id = " + strconv.Itoa(id))
    if err != nil {
        fmt.Println(err)
        return
    }

    for rows.Next() {
        var id int
        var user string
        var pwd string
        rows.Scan(&id, &user, &pwd)
        fmt.Println("id:", id, "user:", user, "pwd:", pwd)
    }
    rows.Close()
}

//插入
func insert(db *sql.DB, user, pwd string) {
    stmt, err := db.Prepare("insert into user set UserName=?,Password=?")
    if err != nil {
        fmt.Println(err)
    }
    res, err := stmt.Exec("peter", "panlei")
    id, err := res.LastInsertId()
    fmt.Println(id)
}

连接池

因为Gorm的连接池就是使用database/sql包中的连接池,所以这里我们需要学习一下包里的连接池的源码实现。其实所有连接池最重要的就是连接池对象、获取函数、释放函数下面来看一下database/sql中的连接池。

DB对象

type DB struct {
    //数据库实现驱动
    driver driver.Driver
    dsn    string
    numClosed uint64
    // 锁
    mu           sync.Mutex // protects following fields
    // 空闲连接
    freeConn     []*driverConn
    //阻塞请求队列,等连接数达到最大限制时,后续请求将插入此队列等待可用连接
    connRequests map[uint64]chan connRequest
    // 记录下一个key用于connRequests map的key
    nextRequest  uint64 // Next key to use in connRequests.
    numOpen      int    // number of opened and pending open connections
    
    openerCh    chan struct{}
    closed      bool
    dep         map[finalCloser]depSet
    lastPut     map[*driverConn]string 
    // 最大空闲连接数
    maxIdle     int                    
    // 最大打开连接数
    maxOpen     int  
    // 连接最大存活时间
    maxLifetime time.Duration          
    cleanerCh   chan struct{}
}

获取方法

func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    db.mu.Lock()
    if db.closed {
        db.mu.Unlock()
        return nil, errDBClosed
    }
    // Check if the context is expired.
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime

    // 查看是否有空闲的连接 如果有则直接使用空闲连接
    numFree := len(db.freeConn)
    if strategy == cachedOrNewConn && numFree > 0 {
        // 取出数据第一个
        conn := db.freeConn[0]
        // 复制数组,去除第一个连接
        copy(db.freeConn, db.freeConn[1:])
        db.freeConn = db.freeConn[:numFree-1]
        conn.inUse = true
        db.mu.Unlock()
        if conn.expired(lifetime) {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }

    // 判断是否超出最大连接数 
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // 创建一个chan 
        req := make(chan connRequest, 1)
        // 获取下一个request 作为map 中的key
        reqKey := db.nextRequestKeyLocked()
        db.connRequests[reqKey] = req
        db.mu.Unlock()

        // Timeout the connection request with the context.
        select {
        case <-ctx.Done():
            // Remove the connection request and ensure no value has been sent
            // on it after removing.
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()
            select {
            default:
            case ret, ok := <-req:
                if ok {
                    db.putConn(ret.conn, ret.err)
                }
            }
            return nil, ctx.Err()
        // 如果没有取消则从req chan中获取数据 阻塞主一直等待有conn数据传入
        case ret, ok := <-req:
            if !ok {
                return nil, errDBClosed
            }
            // 判断超时 
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            return ret.conn, ret.err
        }
    }
    
    db.numOpen++ // optimistically
    db.mu.Unlock()
    // 调用driver的Open方法建立连接
    ci, err := db.driver.Open(db.dsn)
    if err != nil {
        db.mu.Lock()
        db.numOpen-- // correct for earlier optimism
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        return nil, err
    }
    db.mu.Lock()
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
        inUse:     true,
    }
    db.addDepLocked(dc, dc)
    db.mu.Unlock()
    return dc, nil
}

释放连接方法

// 释放连接
func (db *DB) putConn(dc *driverConn, err error) {
    db.mu.Lock()
    if !dc.inUse {
        if debugGetPut {
            fmt.Printf("putConn(%v) DUPLICATE was: %s\n\nPREVIOUS was: %s", dc, stack(), db.lastPut[dc])
        }
        panic("sql: connection returned that was never out")
    }
    if debugGetPut {
        db.lastPut[dc] = stack()
    }
    // 设置已经在使用中
    dc.inUse = false

    for _, fn := range dc.onPut {
        fn()
    }
    dc.onPut = nil
    // 判断连接是否有错误 
    if err == driver.ErrBadConn {
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        dc.Close()
        return
    }
    if putConnHook != nil {
        putConnHook(db, dc)
    }
    // 调用方法 释放连接
    added := db.putConnDBLocked(dc, nil)
    db.mu.Unlock()
    // 判断如果没有加到了空闲列表中 dc关闭
    if !added {
        dc.Close()
    }
}

func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
    if db.closed {
        return false
    }
    if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        return false
    }
    // 如果等待chan列表大于0 
    if c := len(db.connRequests); c > 0 {
        var req chan connRequest
        var reqKey uint64
        // 获取map 中chan和key
        for reqKey, req = range db.connRequests {
            break
        }
        // 从列表中删除chan 
        delete(db.connRequests, reqKey) // Remove from pending requests.
        if err == nil {
            dc.inUse = true
        }
        // 把连接传入chan中 让之前获取连接被阻塞的获取函数继续
        req <- connRequest{
            conn: dc,
            err:  err,
        }
        return true
    } else if err == nil && !db.closed && db.maxIdleConnsLocked() > len(db.freeConn) {
        // 如果没有等待列表,则把连接放到空闲列表中
        db.freeConn = append(db.freeConn, dc)
        db.startCleanerLocked()
        return true
    }
    return false
}

连接池的实现有很多方法,在database/sql包中使用的是chan阻塞 使用map记录等待列表,等到有连接释放的时候再把连接传入等待列表中的chan 不在阻塞返回连接。

之前我们看到的Redigo是使用一个chan 来阻塞,然后释放的时候放入空闲列表,在往这一个chan中传入struct{}{},让程序继续 获取的时候再从空闲列表中获取。并且使用的是链表的结构来存储空闲列表。

总结

database/sql 是对于mysql驱动的封装,然而Gorm则是对于database/sql的再次封装。让我们可以更加简单的实现对于mysql数据库的操作。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Is Parallel Programming Hard, And, If So, What Can You Do About

Is Parallel Programming Hard, And, If So, What Can You Do About

Paul E. McKenney

The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具