You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

30 KiB

26GORM数据库的使用必不可少

你好,我是轩脉刃。

上一节课我们梳理了Gorm的核心逻辑也通过思维导图详细分析了Gorm的源码搞清楚它是如何封装database/sql的。这节课我们就要思考和操作如何将Gorm融合进入hade框架了。

Gorm的使用分为两个部分数据库的连接和数据库的操作。

对于数据库操作接口的封装Gorm已经做的非常好了它在gorm.DB中定义了非常多的对数据库的操作接口这些接口已经是非常易用了而且每个操作接口在官方文档中都有对应的说明和使用教程。比如在DB的操作接口列表中我们可以看到常用的增删改查的逻辑

func (db *DB) Create(value interface{}) (tx *DB)

func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

func (db *DB) Get(key string) (interface{}, bool)

func (db *DB) Update(column string, value interface{}) (tx *DB)

同时,官方首页的例子也把获取到DB后的增删改查操作显示很清楚了建议你在浏览器收藏这个Gorm的说明文档因为在具体的应用开发中你会经常参考使用它的。

所以今天我们要做的事情就是封装Gorm的数据库连接部分。

ORM服务

按照“一切皆服务”的思想我们也计划将Gorm封装为一个服务。而服务三要素是服务接口、服务提供者、服务实例化。我们先来定义ORM服务接口。

服务接口

这个服务接口并不复杂它的唯一任务就是能够初始化出gorm.DB 实例。回顾上节课说的Gorm初始化gorm.DB的方法

  dsn := "xxxxxxx"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

参数看起来就这两个部分DSN和gorm.Config。

不过我们希望设计一个hade框架自定义的配置结构将所有创建连接需要的配置项整合起来。所以除了DSN和gorm.Config这两个配置项其实还需要加上连接池的配置就是上节课说的database/sql中提供的对连接池的配置信息。再回顾一下这四个影响底层创建连接池设置的配置信息

// 设置连接的最大空闲时长
func (db *DB) SetConnMaxIdleTime(d time.Duration)
// 设置连接的最大生命时长
func (db *DB) SetConnMaxLifetime(d time.Duration)
// 设置最大空闲连接数
func (db *DB) SetMaxIdleConns(n int)
// 设置最大打开连接数
func (db *DB) SetMaxOpenConns(n int)

所以可以定义这么一个DBConfig结构将所有的创建DB相关的配置都放在这里面。代码在framework/contract/orm.go中

// DBConfig 代表数据库连接的所有配置
type DBConfig struct {
   // 以下配置关于dsn
   WriteTimeout string `yaml:"write_timeout"` // 写超时时间
   Loc          string `yaml:"loc"`           // 时区
   Port         int    `yaml:"port"`          // 端口
   ReadTimeout  string `yaml:"read_timeout"`  // 读超时时间
   Charset      string `yaml:"charset"`       // 字符集
   ParseTime    bool   `yaml:"parse_time"`    // 是否解析时间
   Protocol     string `yaml:"protocol"`      // 传输协议
   Dsn          string `yaml:"dsn"`           // 直接传递dsn如果传递了其他关于dsn的配置均无效
   Database     string `yaml:"database"`      // 数据库
   Collation    string `yaml:"collation"`     // 字符序
   Timeout      string `yaml:"timeout"`       // 连接超时时间
   Username     string `yaml:"username"`      // 用户名
   Password     string `yaml:"password"`      // 密码
   Driver       string `yaml:"driver"`        // 驱动
   Host         string `yaml:"host"`          // 数据库地址

   // 以下配置关于连接池
   ConnMaxIdle     int    `yaml:"conn_max_idle"`     // 最大空闲连接数
   ConnMaxOpen     int    `yaml:"conn_max_open"`     // 最大连接数
   ConnMaxLifetime string `yaml:"conn_max_lifetime"` // 连接最大生命周期
   ConnMaxIdletime string `yaml:"conn_max_idletime"` // 空闲最大生命周期

   // 以下配置关于gorm
   *gorm.Config // 集成gorm的配置
}

其中DSN是一个复杂的字符串。但我们又不希望使用者直接设置这些复杂字符串来进行传递所以这里设置了多个字段来生成这个DSN。

另外上节课也说过DSN并没有一个标准的格式约定不同的数据库可能有不同的解析所以也同时保留直接设置DSN的权限如果用户手动设置了Dsn字段那么其他关于Dsn的字段设置均无效。

所以这里同时需要实现一个方法使用DBConfig来生成最终使用的字符串Dsn使用上节课介绍的 github.com/go-sql-driver/mysql 库,就能很方便地实现了。我们继续写:

import (
   "github.com/go-sql-driver/mysql"
   ...
)

// FormatDsn 生成dsn
func (conf *DBConfig) FormatDsn() (string, error) {
   port := strconv.Itoa(conf.Port)
   timeout, err := time.ParseDuration(conf.Timeout)
   if err != nil {
      return "", err
   }
   readTimeout, err := time.ParseDuration(conf.ReadTimeout)
   if err != nil {
      return "", err
   }
   writeTimeout, err := time.ParseDuration(conf.WriteTimeout)
   if err != nil {
      return "", err
   }
   location, err := time.LoadLocation(conf.Loc)
   if err != nil {
      return "", err
   }
   driverConf := &mysql.Config{
      User:         conf.Username,
      Passwd:       conf.Password,
      Net:          conf.Protocol,
      Addr:         net.JoinHostPort(conf.Host, port),
      DBName:       conf.Database,
      Collation:    conf.Collation,
      Loc:          location,
      Timeout:      timeout,
      ReadTimeout:  readTimeout,
      WriteTimeout: writeTimeout,
      ParseTime:    conf.ParseTime,
   }
   return driverConf.FormatDSN(), nil
}

可以看到Gorm配置我们使用结构嵌套的方式将gorm.Config直接嵌套进入DBConfig中。你可以琢磨下这种写法它有两个好处。

一是可以直接设置DBConfig来设置gorm.Config。比如这个函数是可行的它直接设置config.DryRun就是直接设置gorm.Config

func(container framework.Container, config *contract.DBConfig) error {
   config.DryRun = true
   return nil
}

二是DBConfig继承了*gorm.Config的所有方法。比如这段代码,我们来理解一下:

config := &contract.DBConfig{}
db, err = gorm.Open(mysql.Open(config.Dsn), config)

还记得gorm.Open的第二个参数是Option么它是一个接口需要实现Apply和AfterInitialize方法而我们的DBConfig并没有显式实现这两个方法。但是它嵌套了实现了这两个方法的*gorm.Config所以默认DB.Config也就实现了这两个方法。

type Option interface {
   Apply(*Config) error
   AfterInitialize(*DB) error
}

现在gorm.Open的两个参数DSN和gorm.Config都封装在DBConfig中而修改DBConfig的方法我们封装为DBOption。

如何让设置DBOption的方法更为优雅呢这里就使用到上节课刚学到的Option可变参数的编程方法了。定义一个DBOption的结构它代表一个可以对DBConfig进行设置的方法这个结构作为获取ORM服务GetDB方法的参数。在framework/contract/orm.go中

package contract

// ORMKey 代表 ORM的服务
const ORMKey = "hade:orm"

// ORMService 表示传入的参数
type ORMService interface {
   GetDB(option ...DBOption) (*gorm.DB, error)
}

// DBOption 代表初始化的时候的选项
type DBOption func(container framework.Container, config *DBConfig) error

这样就能通过设置不同的方法来对DBConfig进行配置。

比如要设置DBConfig中gorm.Config的DryRun空跑字段设计了这么一个方法在framework/provider/orm/config.go中

// WithDryRun 设置空跑模式
func WithDryRun() contract.DBOption {
   return func(container framework.Container, config *contract.DBConfig) error {
      config.DryRun = true
      return nil
   }
}

之后在使用ORM服务的时候我们就可以这样设置

gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
// 可变参数为WithDryRun()
db, err := gormService.GetDB(orm.WithDryRun())

服务提供者

下一步来完成服务提供者,我们也并不需要过于复杂的设计,只要注意一下两点:

  • ORM服务一定是要延迟加载的因为这个服务并不是一个基础服务。如果设置为非延迟加载在框架启动的时候就会去建立这个服务这并不是我们想要的。所以我们设计ORM的provider的时候需要将IsDefer函数设置为true。
  • 第二点考虑到我们后续会使用container中的配置服务来创建具体的gorm.DB实例传递一个container是必要的。

所以具体的服务提供者代码如下在framework/provider/orm/provider.go中

package orm

import (
   "github.com/gohade/hade/framework"
   "github.com/gohade/hade/framework/contract"
)

// GormProvider 提供App的具体实现方法
type GormProvider struct {
}

// Register 注册方法
func (h *GormProvider) Register(container framework.Container) framework.NewInstance {
   return NewHadeGorm
}

// Boot 启动调用
func (h *GormProvider) Boot(container framework.Container) error {
   return nil
}

// IsDefer 是否延迟初始化
func (h *GormProvider) IsDefer() bool {
   return true
}

// Params 获取初始化参数
func (h *GormProvider) Params(container framework.Container) []interface{} {
   return []interface{}{container}
}

// Name 获取字符串凭证
func (h *GormProvider) Name() string {
   return contract.ORMKey
}

服务实例化

服务实例化是今天的重点内容我们先把Gorm的配置结构和日志结构的准备工作完成再写稍微复杂一点的具体ORM服务的实例 HadeGorm。

配置

前面定义了hade框架专属的DBConfig配置结构如何设置它是一个需要讲究的问题。

虽然已经设计了一种修改配置文件的方式就是通过GetDB中的Option参数来设置。但是每个字段都这么设置又非常麻烦我们自然会想到使用配置文件来配置这个结构。另外如果要连接多个数据库每个数据库都进行同样的配置还是颇为麻烦是不是可以有个默认配置呢

于是我们的配置文件可以这样设计:在 database.yaml 中保存数据库的默认值如果想对某个数据库连接有单独的配置可以用内嵌yaml结构的方式来进行配置。看下面这个配置例子

conn_max_idle: 10 # 通用配置,连接池最大空闲连接数
conn_max_open: 100 # 通用配置,连接池最大连接数
conn_max_lifetime: 1h # 通用配置,连接数最大生命周期
protocol: tcp # 通用配置,传输协议
loc: Local # 通用配置,时区

default:
    driver: mysql # 连接驱动
    dsn: "" # dsn,如果设置了dsn, 以下的所有设置都不生效
    host: localhost # ip地址
    port: 3306 # 端口
    database: coredemo # 数据库
    username: jianfengye # 用户名
    password: "123456789" # 密码
    charset: utf8mb4 # 字符集
    collation: utf8mb4_unicode_ci # 字符序
    timeout: 10s # 连接超时
    read_timeout: 2s # 读超时
    write_timeout: 2s # 写超时
    parse_time: true # 是否解析时间
    protocol: tcp # 传输协议
    loc: Local # 时区
    conn_max_idle: 10 # 连接池最大空闲连接数
    conn_max_open: 20 # 连接池最大连接数
    conn_max_lifetime: 1h # 连接数最大生命周期

read:
    driver: mysql # 连接驱动
    dsn: "" # dsn,如果设置了dsn, 以下的所有设置都不生效
    host: localhost # ip地址
    port: 3306 # 端口
    database: coredemo # 数据库
    username: jianfengye # 用户名
    password: "123456789" # 密码
    charset: utf8mb4 # 字符集
    collation: utf8mb4_unicode_ci # 字符序

在这个database.yaml中我们配置了database.default和database.read两个数据源。database.read数据源并没有设置诸如时区loc、连接池conn_max_open配置这些缺省的配置要从databse.yaml的根结构中获取。

要实现这个也并不难先在framework/provider/orm/service.go中实现一个GetBaseConfig方法来读取database.yaml根目录的结构

// GetBaseConfig 读取database.yaml根目录结构
func GetBaseConfig(c framework.Container) *contract.DBConfig {

   configService := c.MustMake(contract.ConfigKey).(contract.Config)
   logService := c.MustMake(contract.LogKey).(contract.Log)
   
   config := &contract.DBConfig{}
   // 直接使用配置服务的load方法读取,yaml文件
   err := configService.Load("database", config)
   if err != nil {
      // 直接使用logService来打印错误信息
      logService.Error(context.Background(), "parse database config error", nil)
      return nil
   }
   return config
}


然后设计一个根据配置路径加载某个配置结构的方法。这里这个方法一定是在具体初始化某个DB实例的时候使用到所以要封装为一个Option结构写在framework/provider/orm/config.go中

// WithConfigPath 加载配置文件地址
func WithConfigPath(configPath string) contract.DBOption {
   return func(container framework.Container, config *contract.DBConfig) error {
      configService := container.MustMake(contract.ConfigKey).(contract.Config)
        // 加载configPath配置路径
      if err := configService.Load(configPath, config); err != nil {
         return err
      }
      return nil
   }
}

现在对于使用者来说要初始化一个配置路径为database.default的数据库就可以这么使用

gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
db, err := gormService.GetDB(orm.WithConfigPath("database.default"), orm.WithDryRun())

日志

配置项设计清楚了我们再来思考下日志这块。上一章介绍过了Gorm是有自己的输出规范的在初始化参数 gorm.Config 中定义了一个日志输出接口Interface。我们来仔细看下这个接口的定义

const (
   Silent LogLevel = iota + 1
   Error
   Warn
   Info
)

// Interface logger interface
type Interface interface {
   LogMode(LogLevel) Interface // 日志级别
   Info(context.Context, string, ...interface{})
   Warn(context.Context, string, ...interface{})
   Error(context.Context, string, ...interface{})
   Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
}

Gorm接口的日志级别分类比较简单Info、Warn、Error、Trace。恰巧这几个日志级别都在我们hade框架定义的7个日志级别中所以完全可以将Gorm的这几个级别映射到hade的日志级别中。也就是说Gorm打印的Info级别日志输出到hade的Info日志中、error日志输出到hade的error日志中。

至于Gorm提供的一个LogMode来调整日志级别由于我们的hade框架已经可以通过配置进行日志级别设置了所以LogMode函数对我们来说是没有什么意义的。

了解Gorm的日志接口之后我们明确了接下来要做的事情实现一个Gorm的日志实现类但是这个日志实现类中的每个方法都用 hade 的日志服务来实现

我们在framework/provider/orm/logger.go中定义一个OrmLogger结构它带有一个logger属性这个logger属性存放的是hade容器中的log服务

// OrmLogger orm的日志实现类, 实现了gorm.Logger.Interface
type OrmLogger struct {
   logger contract.Log // 有一个logger对象存放hade的log服务
}

// NewOrmLogger 初始化一个ormLogger,
func NewOrmLogger(logger contract.Log) *OrmLogger {
   return &OrmLogger{logger: logger}
}

它实现了Gorm的Logger.Interface 接口。其中LogMode什么都不做Info、Error、Warn、Trace 分别对应hade容器中log服务的Info、Error、Warn、Trace方法

// Info 对接hade的info输出
func (o *OrmLogger) Info(ctx context.Context, s string, i ...interface{}) {
   fields := map[string]interface{}{
      "fields": i,
   }
   o.logger.Info(ctx, s, fields)
}

// Warn 对接hade的Warn输出
func (o *OrmLogger) Warn(ctx context.Context, s string, i ...interface{}) {
   fields := map[string]interface{}{
      "fields": i,
   }
   o.logger.Warn(ctx, s, fields)
}

// Error 对接hade的Error输出
func (o *OrmLogger) Error(ctx context.Context, s string, i ...interface{}) {
   fields := map[string]interface{}{
      "fields": i,
   }
   o.logger.Error(ctx, s, fields)
}

// Trace 对接hade的Trace输出
func (o *OrmLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
   sql, rows := fc()
   elapsed := time.Since(begin)
   fields := map[string]interface{}{
      "begin": begin,
      "error": err,
      "sql":   sql,
      "rows":  rows,
      "time":  elapsed,
   }

   s := "orm trace sql"
   o.logger.Trace(ctx, s, fields)
}

这里稍微注意下Trace方法Gorm的Trace方法的参数中有传递时间戳begin这个时间戳代表SQL执行的开始时间而在函数中使用time.Now获取到当前时间之后两个相减我们可以获取到这个SQL的实际执行时间然后作为hade 日志服务的fields map的一个字段输出。除了Trace其他几个基本上简单封装hade的日志服务方法就好了。

服务实例

好了到现在Gorm的配置结构和日志结构也完成了。万事俱备下面我们就开始写具体的ORM服务的实例 HadeGorm在framework/provider/orm/service.go中。

首先定义实现contract.ORMService的结构HadeGorm。要明确一点我们会使用这个结构来生成不同数据库的gorm.DB结构所以这个HadeGorm是一个与某个数据库设置无关的结构而且它应该对单个数据库是一个单例模式即在一个服务中我从HadeGorm两次获取到的default数据库的gorm.DB是同一个。

设置HadeGrom结构如下

// HadeGorm 代表hade框架的orm实现
type HadeGorm struct {
   container framework.Container // 服务容器
   dbs       map[string]*gorm.DB // key为dsn, value为gorm.DB连接池

   lock *sync.RWMutex
}

dbs就是为了单例存在它的key直接设计为一个string也就是连接数据库的DSN字符串而value就是gorm.DB结构。

这样我们在拿到一个DSN的时候从这个map中就能判断出是否已经实例化过这个数据库对应的gorm.DB了如果没有实例化过就实例化一个gorm.DB并且将这个实例挂到这个map中。不过这个逻辑会对dbs有并发修改操作所以这里要使用一个读写锁来锁住这个dbs的修改

对应实例化HadeGorm的方法为NewHadeGorm它的具体实现就是初始化HadeGorm中的每个字段。继续写入这段

// NewHadeGorm 代表实例化Gorm
func NewHadeGorm(params ...interface{}) (interface{}, error) {
   container := params[0].(framework.Container)
   dbs := make(map[string]*gorm.DB)
   lock := &sync.RWMutex{}
   return &HadeGorm{
      container: container,
      dbs:       dbs,
      lock:      lock,
   }, nil
}

重头戏在GetDB方法的实现上。

首先初始化orm.Config其中包括从配置中获取设置项也包括初始化内部的Gorm然后将GetDB的option参数作用于初始化的orm.Config修改默认配置通过orm.Config生成DSN字符串。

  // 读取默认配置
    config := GetBaseConfig(app.container)

    logService := app.container.MustMake(contract.LogKey).(contract.Log)

    // 设置Logger
    ormLogger := NewOrmLogger(logService)
    config.Config = &gorm.Config{
        Logger: ormLogger,
    }

    // option对opt进行修改
    for _, opt := range option {
        if err := opt(app.container, config); err != nil {
            return nil, err
        }
    }

之后根据dsn字符串判断数据库实例gorm.DB是否已经存在了。如果存在直接返回gorm.DB如果不存在需要实例化gorm.DB这一步逻辑稍微复杂一点

  • 根据配置项orm.Config中的不同驱动来实例化gorm.DB支持MySQL/Postgres/SQLite/SQL Server/ClickHouse
  • 根据配置项orm.Config中的连接池配置设置gorm.DB的连接池
  • 将实例化后的gorm.DB和DSN放入map映射中
  • 返回实例化后的gorm.DB

代码如下:

// 如果最终的config没有设置dsn,就生成dsn
    if config.Dsn == "" {
        dsn, err := config.FormatDsn()
        if err != nil {
            return nil, err
        }
        config.Dsn = dsn
    }

    // 判断是否已经实例化了gorm.DB
    app.lock.RLock()
    if db, ok := app.dbs[config.Dsn]; ok {
        app.lock.RUnlock()
        return db, nil
    }
    app.lock.RUnlock()

    // 没有实例化gorm.DB那么就要进行实例化操作
    app.lock.Lock()
    defer app.lock.Unlock()

    // 实例化gorm.DB
    var db *gorm.DB
    var err error
    switch config.Driver {
    case "mysql":
        db, err = gorm.Open(mysql.Open(config.Dsn), config)
    case "postgres":
        db, err = gorm.Open(postgres.Open(config.Dsn), config)
    case "sqlite":
        db, err = gorm.Open(sqlite.Open(config.Dsn), config)
    case "sqlserver":
        db, err = gorm.Open(sqlserver.Open(config.Dsn), config)
    case "clickhouse":
        db, err = gorm.Open(clickhouse.Open(config.Dsn), config)
    }

    // 设置对应的连接池配置
    sqlDB, err := db.DB()
    if err != nil {
        return db, err
    }

    if config.ConnMaxIdle > 0 {
        sqlDB.SetMaxIdleConns(config.ConnMaxIdle)
    }
    if config.ConnMaxOpen > 0 {
        sqlDB.SetMaxOpenConns(config.ConnMaxOpen)
    }
    if config.ConnMaxLifetime != "" {
        liftTime, err := time.ParseDuration(config.ConnMaxLifetime)
        if err != nil {
            logger.Error(context.Background(), "conn max lift time error", map[string]interface{}{
                "err": err,
            })
        } else {
            sqlDB.SetConnMaxLifetime(liftTime)
        }
    }

    if config.ConnMaxIdletime != "" {
        idleTime, err := time.ParseDuration(config.ConnMaxIdletime)
        if err != nil {
            logger.Error(context.Background(), "conn max idle time error", map[string]interface{}{
                "err": err,
            })
        } else {
            sqlDB.SetConnMaxIdleTime(idleTime)
        }
    }

    // 挂载到map中结束配置
    if err != nil {
        app.dbs[config.Dsn] = db
    }

    return db, err

如果前面的内容都理解了,这段代码实现也没有什么难点了。唯一要注意的地方就是锁的使用,由于对存在gorm.DB的map是读多写少所以这里也是使用读写锁在读取的时候加了一个读锁如果map中没有我们要的gorm.DB先把读锁解开再加一个写锁初始化完gorm.DB、保存进入map映射后再把写锁解开。这样能有效防止对map的并发读写。

完整的GetDB方法可以参考GitHub上的framework/provider/orm/service.go

最后记得去业务代码main.go中把我们的GormProvider注入服务容器

func main() {
   // 初始化服务容器
   container := framework.NewHadeContainer()
   ...
   container.Bind(&orm.GormProvider{})

   ...

   // 运行root命令
   console.RunCommand(container)
}

整个Gorm就已经结合到hade框架中了。

测试

下面来做一下测试。我们用真实的MySQL进行测试。当然你需要在本机/远端/Docker搭建一个MySQL至于怎么搭建教程网上有很多了这里就不详细描述。

我用的是Mac使用homebrew 能很方便搭建一个MySQL服务。我的MySQL实例搭建在本机的3306端口并且搭建完成之后我创建了一个coredemo的database数据库

所以我的配置文件config/development/database.yaml配置如下

conn_max_idle: 10 # 通用配置,连接池最大空闲连接数
conn_max_open: 100 # 通用配置,连接池最大连接数
conn_max_lifetime: 1h # 通用配置,连接数最大生命周期
protocol: tcp # 通用配置,传输协议
loc: Local # 通用配置,时区

default:
    driver: mysql # 连接驱动
    dsn: "" # dsn,如果设置了dsn, 以下的所有设置都不生效
    host: localhost # ip地址
    port: 3306 # 端口
    database: coredemo # 数据库
    username: jianfengye # 用户名
    password: "123456789" # 密码
    charset: utf8mb4 # 字符集
    collation: utf8mb4_unicode_ci # 字符序
    timeout: 10s # 连接超时
    read_timeout: 2s # 读超时
    write_timeout: 2s # 写超时
    parse_time: true # 是否解析时间
    protocol: tcp # 传输协议
    loc: Local # 时区

我们想在coredemo数据库中增加一个user表按照Gorm的规范需要先定义一个数据结构User。在app/http/module/demo/model.go中

// User is gorm model
type User struct {
   ID           uint
   Name         string
   Email        *string
   Age          uint8
   Birthday     *time.Time
   MemberNumber sql.NullString
   ActivatedAt  sql.NullTime
   CreatedAt    time.Time
   UpdatedAt    time.Time
}

然后在应用目录app/http/module/demo/api_orm.go中定义了一个新的路由方法DemoOrm在这个方法中我们先从容器中获取到gorm.DB的实例然后使用db.AutoMigrate 同步数据表user。

如果第一次执行的时候数据库中没有表user它会自动创建user表然后分别调用db.Create、db.Save、db.First、db.Delete来对user表进行增删改查操作

// DemoOrm Orm的路由方法
func (api *DemoApi) DemoOrm(c *gin.Context) {
    logger := c.MustMakeLog()
    logger.Info(c, "request start", nil)

    // 初始化一个orm.DB
    gormService := c.MustMake(contract.ORMKey).(contract.ORMService)
    db, err := gormService.GetDB(orm.WithConfigPath("database.default"))
    if err != nil {
        logger.Error(c, err.Error(), nil)
        c.AbortWithError(50001, err)
        return
    }
    db.WithContext(c)

    // 将User模型创建到数据库中
    err = db.AutoMigrate(&User{})
    if err != nil {
        c.AbortWithError(500, err)
        return
    }
    logger.Info(c, "migrate ok", nil)

    // 插入一条数据
    email := "foo@gmail.com"
    name := "foo"
    age := uint8(25)
    birthday := time.Date(2001, 1, 1, 1, 1, 1, 1, time.Local)
    user := &User{
        Name:         name,
        Email:        &email,
        Age:          age,
        Birthday:     &birthday,
        MemberNumber: sql.NullString{},
        ActivatedAt:  sql.NullTime{},
        CreatedAt:    time.Now(),
        UpdatedAt:    time.Now(),
    }
    err = db.Create(user).Error
    logger.Info(c, "insert user", map[string]interface{}{
        "id":  user.ID,
        "err": err,
    })

    // 更新一条数据
    user.Name = "bar"
    err = db.Save(user).Error
    logger.Info(c, "update user", map[string]interface{}{
        "err": err,
        "id":  user.ID,
    })

    // 查询一条数据
    queryUser := &User{ID: user.ID}

    err = db.First(queryUser).Error
    logger.Info(c, "query user", map[string]interface{}{
        "err":  err,
        "name": queryUser.Name,
    })

    // 删除一条数据
    err = db.Delete(queryUser).Error
    logger.Info(c, "delete user", map[string]interface{}{
        "err": err,
        "id":  user.ID,
    })
    c.JSON(200, "ok")
}

记得修改app/http/module/demo/api.go中的路由注册

func Register(r *gin.Engine) error {
   api := NewDemoApi()
   ...
   r.GET("/demo/orm", api.DemoOrm)
   return nil
}

现在,使用 ./hade build self 来重新编译hade文件使用 ./hade app start 启动服务,并挂起在控制台,日志会输出到控制台。浏览器调用 http://localhost:8888/demo/orm ,控制台打印日志如下:

可以清晰地通过trace日志看到底层的Insert/Update/Select/Delete的操作并且可以通过time字段看到这个请求的具体耗时。到这里Gorm融合hade框架就验证完成了。

本节课我们主要修改了framework目录下的contract/orm.go 和 provider/orm 目录。目录截图如下,供对比查看,所有代码都已经上传到geekbang/25分支了。

小结

对于Gorm这样比较庞大的库要把Gorm完美集成到hade框架更好地支持业务对数据库频繁的增删改查操作我们并不是一开始就动手修改代码而是先把Gorm的实例化部分的源码都理清楚了再动手集成才不会出现问题。

现在我们可以在hade框架中方便获取到gorm.DB了。但是在具体开发业务的时候如何使用好Gorm来为业务服务也是一个非常值得花心思研究的课题。好在我们的技术选型是目前Golang业界最火的Gorm网络上关于如何使用Gorm的课程有非常多了在具体开发业务的时候你可以自己参考和研究。

思考题

在ORM框架中model层的存放位置一直是个很有争论的话题。比如geekbang/25 分支上model层的User结构我存放在app/http/module/demo中有同学会觉得model层放在 app/http/model目录比较好么具体model是否应该单独作为一个文件夹出来呢

欢迎在留言区分享你的思考。感谢你的收听,如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习。我们下节课见~