记录
gorm
的一些基本用法,使用原生的话操作比较繁琐,使用gorm
的话可以让自己减少反复的写一些sql语句
gorm的一些特性
提示:以下是本篇文章正文内容,下面案例可供参考
package main
import (
"fmt"
"sync"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
db *gorm.DB
linkOnce sync.Once
)
// 用于连接数据库
func linkDb() {
linkOnce.Do(func() {
dsn := "root:root@tcp(localhost:3306)/my_sql?charset=utf8mb4&parseTime=True&loc=Local"
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
})
}
func init() {
linkDb()
}
- 连接mysql,并且用全局变量进行保存因为后面大量的操作需要用到
db
sync.Once
保证全局唯一连接
配置
logrus
,并设定文件的创建的函数,这个与另一篇gin
日志中间件的几乎一样就不再详细讲解。
package main
import (
"fmt"
"os"
"path"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type MyWriter struct {
mlog *logrus.Logger
}
// 初始化一个 MyWriter
func NewMyWriter() *MyWriter {
logger := logrus.New()
logFilePath := ""
if dir, err := os.Getwd(); err == nil {
logFilePath = dir + "/logs/"
}
// 创建日志文件夹
createFolder(logFilePath)
filenameFormat := time.Now().Format("2006-01-02")
src := createLogFile(logFilePath, filenameFormat)
logger.Out = src
logger.SetLevel(logrus.DebugLevel)
logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
return &MyWriter{mlog: logger}
}
//实现gorm/logger.Writer接口
func (m *MyWriter) Printf(format string, v ...interface{}) {
logstr := fmt.Sprintf(format, v...)
//利用loggus记录日志
m.mlog.Info(logstr)
}
// 检查并且创建日志文件夹
func createFolder(logFilePath string) {
if err := os.MkdirAll(logFilePath, 0777); err != nil {
fmt.Println("文件夹创建失败")
panic(err)
}
}
// |创建| 打开 日志文件
func createLogFile(logFilePath, filenameFormat string) *os.File {
logFileName := filenameFormat + ".log"
fileName := path.Join(logFilePath, logFileName)
// 检查是否能够成功创建日志文件
checkFile := func(filename string) {
// 以时间去命名日志文件
// 先去判断文件名字是否合法
if _, err := os.Stat(filename); err != nil {
if _, err := os.Create(filename); err != nil {
fmt.Println("打开文件失败")
panic(err)
}
}
}
checkFile(fileName)
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
panic(err)
}
return src
}
修改上述数据库的链接,添加一个类似于中间件的东西
// 用于连接数据库
func linkDb() {
linkOnce.Do(func() {
newLogger := logger.New(
NewMyWriter(), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
)
dsn := "root:ywh@tcp(121.41.27.5:3306)/my_sql?charset=utf8mb4&parseTime=True&loc=Local"
var err error
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
})
}
有了上述的数据库连接以及日志记录的基础,下面当然是一些增删改查以及事务的基本操作啦。
首先我们声明一个结构体
Product
, 后面的操作都是基于这个结构体的
type Product struct {
// gorm.Model // 结构体的内嵌,使用的话会默认给你增加几个字段 有
Code string `gorm:"primarykey"`
Price int `gorm:"price"`
}
// 对结构体添加 TableName 方法来设置对应结构体对应相连接的数据表
// 若没有设置默认会将结构体的名称 lower 然后 + s 得到的字符串作为默认的表名
func (t Product) TableName() string {
return "product"
}
gorm.Model
会额外增加id
、created_at
、updated_at
、deleted_at
,然后id
的话是默认作为一个主键的,这些去查看源代码都可以看到。
// 数据表的创建
func useGorm() {
// Migrate the schema
// 可创建对应的表
// 若已存在同名且字段一样的表则不会操作
if err := db.AutoMigrate(&Product{}); err != nil {
panic(err)
}
}
要注意的是创建的表名即为
db.AutoMigrate(&Product{})
中结构体Product
上的methodTableName
如果结构体没有这个method那么就是这个结构体对应的名字的的小写然后加上s。例如这里Product
上没有前面的TableName
method那么创建的表名就是products
单条数据的插入
// 数据的单条插入
func insertData() {
rand.Seed(time.Now().UnixNano())
code := strconv.Itoa(rand.Intn(1000))
product := Product{
Code: "dasd" + code,
Price: rand.Intn(100),
}
// Omit 用于忽略字段
if err := db.Model(&Product{}).Omit("Price").Create(&product).Error; err != nil {
fmt.Println("数据插入失败")
}
// 指定对应的字段插入
// db.Select("Price").Create(&product)
// if err := db.Select("Price").Create(&product).Error; err != nil {
// fmt.Println("数据插入失败")
// }
// 将结构体中的数据全部插入
// if err := db.Model(&Product{}).Create(&product).Error; err != nil {
// fmt.Println("数据插入失败")
// }
}
- 因为在gorm中很好的支持了链式的操作,其实通过
db.Create(&product)
就可以把指定的数据插入到表中,而Model
的作用是可以指定输入插入数据对应的表,如果删除掉Model
而直接执行db..Create(&product)
的话插入表的信息以product
这个数据对应的结构体来指定,所以Model
可加可不加,加上去就以Model
优先。- 字段的筛选,
Omit
可指定插入数据时要忽略的字段- 字段的选择,
Select
可指定插入数据时要保留的字段
method | 作用 |
---|---|
Model | 指定后续要操作的数据表的基本信息(表名 、字段名 ),不使用该方法则以插入数据的结构体来指定 |
Omit | 指定要忽略对的字段名 |
Select | 指定要保留的字段名 |
Create | 执行插入操作 |
多数据一次性插入
// 数据的批量插入
func insertDatas() {
rand.Seed(time.Now().UnixMilli())
var products []Product
for i := 0; i < 10; i++ {
products = append(products, Product{
Code: "dasd" + strconv.Itoa(rand.Intn(1000)),
Price: rand.Intn(100),
})
}
//这样会将所有的数据生成一条sql语句然后执行插入,这个可以观察日志的输出可以看出
if err := db.Model(&Product{}).Create(&products).Error; err != nil {
fmt.Println("数据插入失败")
panic(err)
}
}
有时候可能要插入的数据太多,生成单句sql插入的方式可能性能较差,所以也支持数据的分批的插入
多数据的分批插入
// 数据的批量插入
func insertData() {
rand.Seed(time.Now().UnixMilli())
var products []Product
for i := 0; i < 10; i++ {
products = append(products, Product{
Code: "dasd" + strconv.Itoa(rand.Intn(1000)),
Price: rand.Intn(100),
})
}
// 指定一批最多执行 CreateBatchSize 个数据的插入
db := db.Session(&gorm.Session{CreateBatchSize: 2})
if err := db.Model(&Product{}).Create(&products).Error; err != nil {
fmt.Println("数据插入失败")
panic(err)
}
}
单条数据的删除
// 数据的单条删除
func delData() {
var data Product
if err := db.Where("code = ?", "dasd104").Delete(&data).Error; err != nil {
panic(err)
}
// 根据主键进行删除
db.Delete(&Product{}, "10")
// DELETE FROM users WHERE code = 10;
// 保存删除的数据 仅适用于支持 Returning 的数据库
db.Clauses(clause.Returning{}).Where("code = ?", "dasd945").Delete(&data)
}
与上面的数据插入不同的是这里通过
Delete
方法来指定要对应操作的数据库
数据的多条删除
// 数据的单条删除
func delDatas() {
if err := db.Where("code LIKE ?", "%1%").Delete(&Product{}).Error; err != nil {
panic(err)
}
// 也可以
// db.Delete(&Product{}, "code LIKE ?", "%1%")
}
单列数据的更新
// 对数据进行更新
func updateData() {
product := Product{"dasd706", 10}
// 条件更新
db.Model(&Product{}).Where("code = ?", "dasd706").Update("price", 10)
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;
// 根据 product 中的值进行更新
//db.Model(&product).Update("price", 10)
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;
// 根据条件和 model 的值进行更新
//db.Model(&product).Where("code = ?", "dasd706").Update("price", 10)
}
多列数据的更新
// 对数据进行更新
func updateDatas() {
// Update - update multiple fields
// 是按照对应的主键进行更新的 若没有设置主键则按照第一个字段进行更新
// db.Model(&Product{}).Updates(Product{Price: 2000, Code: "F42"}) // non-zero fields
// 使用多字段的更新
// db.Model(&Product{}).Updates(map[string]interface{}{"Price": 0, "Code": "F42"})
if err := db.Model(&Product{}).Where("Price = ?", 16).Update("Price", 120).Error; err != nil {
panic(err)
}
}
上述没有用到
Model
方法的话,操作的数据表由Update
方法中的结构体指示
单条数据的查询
// 用于数据查询的
func searchData() {
aimData := Product{}
// 无条件查询
// 按照主键升序
if result := db.First(&aimData); result.Error != nil {
panic(result.Error.Error())
} else {
fmt.Println("找到的记录数", result.RowsAffected)
fmt.Println("查找到的数据", aimData)
}
// 获取一条记录,不指定排序字段
// db.Take(&aimData)
// 获取最后一条记录 按照主键的降序
// db.Last(&aimData)
// 条件查询
// 根据主键进行查询
//db.First(&user, "10")
// SELECT * FROM product WHERE code = "10";
}
多条数据的查询
// 用于数据查询的
func searchDatas() {
aimData := []Product{}
if err := db.Model(&Product{}).Where("Price = ?", 29).Find(&aimData).Error; err != nil {
fmt.Println(err.Error())
}
fmt.Println(aimData)
// IN
if err := db.Where("Price IN ?", []int{29, 50, 120}).Find(&aimData).Error; err != nil {
panic(err)
}
fmt.Println(aimData)
// 用map进行查询
// SELECT * FROM product WHERE code = "dasd671" AND price = 5;
if err := db.Where(map[string]interface{}{"code": "dasd671", "price": 5}).Find(&aimData).Error; err != nil {
panic(err)
}
fmt.Println(aimData)
}
上述没有用到
Model
方法的话,操作的数据表由Find
方法中的结构体指示
// 使用事务
func useAffairs() error {
rand.Seed(time.Now().UnixNano())
tx := db.Begin()
defer func() {
// 若发生 panic可恢复
if r := recover(); r != nil {
tx.Rollback()
fmt.Println("回滚")
}
}()
if err := tx.Error; err != nil {
return err
}
if err := tx.Create(&Product{Price: rand.Intn(100), Code: "测试事务" + strconv.Itoa(rand.Intn(1000))}).Error; err != nil {
fmt.Println("发生回滚")
tx.Rollback()
return err
}
if err := tx.Create(&Product{Price: rand.Intn(100), Code: "测试事务" + strconv.Itoa(rand.Intn(1000))}).Error; err == nil {
fmt.Println("发生回滚")
tx.Rollback()
return err
}
return tx.Commit().Error
}
- 在开启事务之后,在上述的函数中若任意一条语句创建失败或者是运行时panic的话都会触发事务的回滚
- 需要注意的是
tx.Commit()
被执行的时候sql语句才真正被执行