数据库
推荐学习:GORM 指南
在这个项目中最大的亮点其实可以切换数据库进行使用。先介绍一下原理,之后就会简单介绍gorm(理性看待,愿意用可以用,多查查文档就好了。)
配置多个数据库
1.数据库参数配置结构体
这里的结构体的目的是给能够通过结构体达到多个数据库配置,而不是写在配置文件。
type ConfigParams struct {
//配置读写分离
Write ConfigParamsDetail //写
Read ConfigParamsDetail //读
}
type ConfigParamsDetail struct { //配置详情
Host string //地址
DataBase string //数据库名称
Port int //端口
Prefix string //前缀
User string //用户名称
Pass string //用户密码
Charset string //编码格式
}
复制代码
2.获取数据的dsn :Data Source Name
// 根据配置参数生成数据库驱动或者自己添加参数配置驱动 dsn
func getDsn(sqlType, readWrite string, dbConf ...ConfigParams) string {
//通过配置文件获取
Host := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Host")
DataBase := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".DataBase")
Port := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + "." + readWrite + ".Port")
User := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".User")
Pass := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Pass")
Charset := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Charset")
//通过函数参数获取 (其实这一块并没有使用)
if len(dbConf) > 0 {
if strings.ToLower(readWrite) == "write" {
if len(dbConf[0].Write.Host) > 0 {
Host = dbConf[0].Write.Host
}
if len(dbConf[0].Write.DataBase) > 0 {
DataBase = dbConf[0].Write.DataBase
}
if dbConf[0].Write.Port > 0 {
Port = dbConf[0].Write.Port
}
if len(dbConf[0].Write.User) > 0 {
User = dbConf[0].Write.User
}
if len(dbConf[0].Write.Pass) > 0 {
Pass = dbConf[0].Write.Pass
}
if len(dbConf[0].Write.Charset) > 0 {
Charset = dbConf[0].Write.Charset
}
} else {
if len(dbConf[0].Read.Host) > 0 {
Host = dbConf[0].Read.Host
}
if len(dbConf[0].Read.DataBase) > 0 {
DataBase = dbConf[0].Read.DataBase
}
if dbConf[0].Read.Port > 0 {
Port = dbConf[0].Read.Port
}
if len(dbConf[0].Read.User) > 0 {
User = dbConf[0].Read.User
}
if len(dbConf[0].Read.Pass) > 0 {
Pass = dbConf[0].Read.Pass
}
if len(dbConf[0].Read.Charset) > 0 {
Charset = dbConf[0].Read.Charset
}
}
}
//配置多个数据库的相关驱动dsn
switch strings.ToLower(sqlType) {
case "mysql":
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", User, Pass, Host, Port, DataBase, Charset)
case "sqlserver", "mssql":
return fmt.Sprintf("server=%s;port=%d;database=%s;user id=%s;password=%s;encrypt=disable", Host, Port, DataBase, User, Pass)
case "postgresql", "postgre", "postgres":
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable TimeZone=Asia/Shanghai", Host, Port, DataBase, User, Pass)
}
return ""
}
复制代码
3.获取数据库指针
// 获取一个数据库方言(Dialector),通俗的说就是根据不同的连接参数,获取具体的一类数据库的连接指针
func getDbDialector(sqlType, readWrite string, dbConf ...ConfigParams) (gorm.Dialector, error) {
var dbDialector gorm.Dialector
//获取数据库驱动的dsn
dsn := getDsn(sqlType, readWrite, dbConf...)
switch strings.ToLower(sqlType) {
case "mysql":
dbDialector = mysql.Open(dsn)
case "sqlserver", "mssql":
dbDialector = sqlserver.Open(dsn)
case "postgres", "postgresql", "postgre":
dbDialector = postgres.Open(dsn)
default:
return nil, errors.New(my_errors.ErrorsDbDriverNotExists + sqlType)
}
return dbDialector, nil
}
复制代码
4.获取数据库驱动,连接数据库
// 获取数据库驱动, 可以通过options 动态参数连接任意多个数据库
func GetSqlDriver(sqlType string, readDbIsOpen int, dbConf ...ConfigParams) (*gorm.DB, error) {
//获取数据库驱动指针
var dbDialector gorm.Dialector
//获取驱动指针
if val, err := getDbDialector(sqlType, "Write", dbConf...); err != nil {
variable.ZapLog.Error(my_errors.ErrorsDialectorDbInitFail+sqlType, zap.Error(err))
} else {
dbDialector = val
}
//通过gorm驱动数据库
gormDb, err := gorm.Open(dbDialector, &gorm.Config{
SkipDefaultTransaction: true,//跳过默认事务 为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。
PrepareStmt: true, //在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率
Logger: redefineLog(sqlType), //拦截、接管 gorm v2 自带日志
})
if err != nil {
//gorm 数据库驱动初始化失败
return nil, err
}
// 如果开启了读写分离,配置读数据库(resource、read、replicas)
// 读写分离配置
if readDbIsOpen == 1 {
if val, err := getDbDialector(sqlType, "Read", dbConf...); err != nil {
variable.ZapLog.Error(my_errors.ErrorsDialectorDbInitFail+sqlType, zap.Error(err))
} else {
dbDialector = val
}
resolverConf := dbresolver.Config{
Replicas: []gorm.Dialector{dbDialector}, // 读 操作库,查询类
Policy: dbresolver.RandomPolicy{}, // sources/replicas 负载均衡策略适用于
}
err = gormDb.Use(dbresolver.Register(resolverConf).SetConnMaxIdleTime(time.Second * 30).
SetConnMaxLifetime(variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+".Read.SetConnMaxLifetime") * time.Second).
SetMaxIdleConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Read.SetMaxIdleConns")).
SetMaxOpenConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Read.SetMaxOpenConns")))
if err != nil {
return nil, err
}
}
// 查询没有数据,屏蔽 gorm v2 包中会爆出的错误
// https://github.com/go-gorm/gorm/issues/3789 此 issue 所反映的问题就是我们本次解决掉的
_ = gormDb.Callback().Query().Before("gorm:query").Register("disable_raise_record_not_found", func(d *gorm.DB) {
d.Statement.RaiseErrorOnNotFound = false
})
// 为主连接设置连接池(43行返回的数据库驱动指针)
if rawDb, err := gormDb.DB(); err != nil {
return nil, err
} else {
//相关配置
//连接池里面的连接最大空闲时长。
rawDb.SetConnMaxIdleTime(time.Second * 30)
//# 连接不活动时的最大生存时间(秒)
rawDb.SetConnMaxLifetime(variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+".Write.SetConnMaxLifetime") * time.Second)
//设置与数据库建立连接的最大数目。
rawDb.SetMaxIdleConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Write.SetMaxIdleConns"))
//设置连接池中的最大闲置连接数。
rawDb.SetMaxOpenConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Write.SetMaxOpenConns"))
return gormDb, nil
}
}
复制代码
5.进行封装,通过不同函数拿到不同的数据库指针
// 获取一个 mysql 客户端
func GetOneMysqlClient() (*gorm.DB, error) {
sqlType := "Mysql"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
// 获取一个 sqlserver 客户端
func GetOneSqlserverClient() (*gorm.DB, error) {
sqlType := "SqlServer"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
// 获取一个 postgresql 客户端
func GetOnePostgreSqlClient() (*gorm.DB, error) {
sqlType := "Postgresql"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
复制代码
6.初始化数据库
// 6.根据配置初始化 gorm mysql 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.Mysql.IsInitGolobalGormMysql") == 1 {
if dbMysql, err := gorm_v2.GetOneMysqlClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbMysql = dbMysql
}
}
// 根据配置初始化 gorm sqlserver 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.Sqlserver.IsInitGolobalGormSqlserver") == 1 {
if dbSqlserver, err := gorm_v2.GetOneSqlserverClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbSqlserver = dbSqlserver
}
}
// 根据配置初始化 gorm postgresql 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.PostgreSql.IsInitGolobalGormPostgreSql") == 1 {
if dbPostgre, err := gorm_v2.GetOnePostgreSqlClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbPostgreSql = dbPostgre
}
}
复制代码
使用数据库
创建获取db工厂
//基础模型
type BaseModel struct {
*gorm.DB `gorm:"-" json:"-"`
Id int64 `gorm:"primarykey" json:"id"`
CreatedAt string `json:"created_at"` //日期时间字段统一设置为字符串即可
UpdatedAt string `json:"updated_at"`
}
复制代码
func UseDbConn(sqlType string) *gorm.DB {
var db *gorm.DB
//获取数据库类型
sqlType = strings.Trim(sqlType, " ")
if sqlType == "" {
sqlType = variable.ConfigGormv2Yml.GetString("Gormv2.UseDbType")
}
//选择数据库的db
switch strings.ToLower(sqlType) {
case "mysql":
if variable.GormDbMysql == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbMysql
case "sqlserver":
if variable.GormDbSqlserver == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbSqlserver
case "postgres", "postgre", "postgresql":
if variable.GormDbPostgreSql == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbPostgreSql
default:
variable.ZapLog.Error(my_errors.ErrorsDbDriverNotExists + sqlType)
}
return db
}
复制代码
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
复制代码
使用
model.CreateUserFactory("").Register(userName, pass, userIp)
复制代码
自定义日志模块
创建日志模块
// 创建自定义日志模块,对 gorm 日志进行拦截、
func redefineLog(sqlType string) gormLog.Interface {
return createCustomGormLog(sqlType,
SetInfoStrFormat("[info] %s\n"), SetWarnStrFormat("[warn] %s\n"), SetErrStrFormat("[error] %s\n"),
SetTraceStrFormat("[traceStr] %s [%.3fms] [rows:%v] %s\n"), SetTracWarnStrFormat("[traceWarn] %s %s [%.3fms] [rows:%v] %s\n"), SetTracErrStrFormat("[traceErr] %s %s [%.3fms] [rows:%v] %s\n"))
}
复制代码
在这里他使用的是Functional Options Patter
去实现的参数配置(相关信息返回的格式化)
ps:这个地方的流程有点点绕
相关博客推荐:Golang中设置函数默认参数的优雅实现
创建对象工厂
// 自定义日志格式, 对 gorm 自带日志进行拦截重写
func createCustomGormLog(sqlType string, options ...Options) gormLog.Interface {
//默认格式输出
var (
infoStr = "%s\n[info] "
warnStr = "%s\n[warn] "
errStr = "%s\n[error] "
traceStr = "%s\n[%.3fms] [rows:%v] %s"
traceWarnStr = "%s %s\n[%.3fms] [rows:%v] %s"
traceErrStr = "%s %s\n[%.3fms] [rows:%v] %s"
)
//logger相关配置
logConf := gormLog.Config{
// 慢 SQL 阈值
SlowThreshold: time.Second * variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+"."+".SlowThreshold"),
//// 日志级别
LogLevel: gormLog.Warn,
// 禁用彩色打印
Colorful: false,
}
//自定义的gorm日志对象,先进行默认处理
log := &logger{
Writer: logOutPut{},
Config: logConf,
infoStr: infoStr,
warnStr: warnStr,
errStr: errStr,
traceStr: traceStr,
traceWarnStr: traceWarnStr,
traceErrStr: traceErrStr,
}
//对日志格式进行修改。
for _, val := range options {
val.apply(log)
}
return log
}
复制代码
这里的option
为什么能够修改默认日志配置?
修改默认配置
//定义操作接口
type Options interface {
apply(*logger)
}
//定义执行函数的类型
type OptionFunc func(log *logger)
//接口的实现函数 ---执行当前类型的函数
func (f OptionFunc) apply(log *logger) {
f(log)
}
// 定义 6 个函数修改内部变量
func SetInfoStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.infoStr = format
})
}
复制代码
流程是这样子的:
createCustomGormLog
的参数类型是Options
,参数可以是只要实现了相关接口的对象。(先明白这一点),你会发现当使用createCustomGormLog
传递的参数是SetInfoStrFormat
函数,因为SetInfoStrFormat
是OptionFunc
的类型(因为他的返回值类型)SetInfoStrFormat
将一个匿名函数通过类型强制转换成OptionFunc
,所以进一步看,匿名函数类型是OptionFunc
,然后因为他实现了apply
接口,所以匿名函数可以直接调用apply
方法apply
方法就是执行当前OptionFunc
这个类型的函数,当调用时候,也就是执行当前的匿名函数- 就更改了
looger
的参数配置
这一块不太好理解,建议多看看,多思考。
我个人不是很喜欢这样方式去实现参数配置。以下是我觉得比较好的,可读性高的实现方式
//定义操作的接口类型
type Options interface {
apply(*logger)
}
//相关方法
func (o OptionFunc) apply(log *logger) {
o.f(log)
}
//这里是一个类(结构体)而不是类型
type OptionFunc struct {
f func(log *logger)
}
//创建结构体
func NewOptionFunc(f func(log *logger)) *OptionFunc {
return &OptionFunc{
f:f,
}
}
//type OptionFunc func(log *logger)
func SetInfoStrFormat(format string) Options {
return NewOptionFunc(func(log *logger) {
log.infoStr = format
})
}
复制代码
自定义logger
根据官方文档,你需要自定义logger你需要实现它定义的接口,在接口里面写你的逻辑代码。
参考 GORM 的 默认 logger 来定义您自己的 logger
type logger struct {
gormLog.Writer
gormLog.Config
infoStr, warnStr, errStr string
traceStr, traceErrStr, traceWarnStr string
}
// LogMode log mode
func (l *logger) LogMode(level gormLog.LogLevel) gormLog.Interface {
newlogger := *l
newlogger.LogLevel = level
return &newlogger
}
// Info print info
func (l logger) Info(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Info {
l.Printf(l.infoStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Warn print warn messages
func (l logger) Warn(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Warn {
l.Printf(l.warnStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Error print error messages
func (l logger) Error(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Error {
l.Printf(l.errStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Trace print sql message
func (l logger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.LogLevel > 0 {
elapsed := time.Since(begin)
switch {
case err != nil && l.LogLevel >= gormLog.Error:
sql, rows := fc()
if rows == -1 {
l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormLog.Warn:
sql, rows := fc()
slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
if rows == -1 {
l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
case l.LogLevel >= gormLog.Info:
sql, rows := fc()
if rows == -1 {
l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
}
}
}
复制代码
如果你看过他的logger的话,你会发现这两者没有太大的差异,那么他是怎么自定义的?好像并不是直接修改这几个接口
创建实现Printf函数的结构体
type Writer interface {
Printf(string, ...interface{})
}
复制代码
因为logger
结构体的gormLog.Writer
的类型,所以这里需要一个实现的Writer
的Printf
接口,然后就可以达到自定义日志的目的。因为后续的使用都是调用你创建那个结构体实现的Printf
的函数
type logOutPut struct{}
//拦截日志,将日志写入
func (l logOutPut) Printf(strFormat string, args ...interface{}) {
logRes := fmt.Sprintf(strFormat, args...)
logFlag := "gorm_v2 日志:"
detailFlag := "详情:"
//通过匹配合成的日志字符串的前缀,然后选择性的写入日志文件。
if strings.HasPrefix(strFormat, "[info]") || strings.HasPrefix(strFormat, "[traceStr]") {
variable.ZapLog.Info(logFlag, zap.String(detailFlag, logRes))
} else if strings.HasPrefix(strFormat, "[error]") || strings.HasPrefix(strFormat, "[traceErr]") {
variable.ZapLog.Error(logFlag, zap.String(detailFlag, logRes))
} else if strings.HasPrefix(strFormat, "[warn]") || strings.HasPrefix(strFormat, "[traceWarn]") {
variable.ZapLog.Warn(logFlag, zap.String(detailFlag, logRes))
}
}
复制代码
总结
数据库自定义日志有两个难点:
- 如何优雅的修改默认参数配置
- 解决方法:
Functional Options Patter
方式去实现
- 解决方法:
- 如何达到自定义日志输出的目的
- 解决方法:创建实现了实现的
Writer
的Printf
接口的结构体,并且给logger
的Writer
类型字段赋值
- 解决方法:创建实现了实现的
gorm
我发现gorm版本升级后,很多都发生了改变,对于这个,只能说去看官方文档吧,一些教程可能就太老了,可能会误导你。没有形成一个规定的话,就只能靠文档去学习,教程什么的就不靠谱,除非这个教程是当前版本的。