Go框架三件套(Web/RPC/GORM)二 | 青训营
2.4.6 First 使用详细
First 是 GORM 中查询数据的一种方法,它可以返回第一条符合条件的数据。使用 First 方法时需要传递一个参数,这个参数可以是模型结构体的指针或接口。
First的使用踩坑:使用 First 时,需要注意查询不到数据会返回 ErrRecordNotFound.使用 Find 查询多条数据,查询不到数据不会返回错误。
使用结构体作为查询条件:当使用结构作为条件查询时,GORM只会查询非零值字段。这意味着如果您的字段值为 0、"、false 或其他零值该字段不会被用于构建查询条件,使用Map 来构建查询条件。
db, err := gorm.Open(mysql.Open(dsn:"username:password@tcp(localhost:9910)/database?charset=utf8"),&gorm.Config{})
if err != nil {
panic(v: "failed to connect database")
}
//获取第一条记录(主键升序),查询不到数据则返回ErrRecordNotFound
u := &User{}
db.First(u) //SELECT * FROM users ORDER BY id LIMIT 1:
//查询多条数据
users := make([]*User,0)
result := db.Where( query: "age > 10").Find(&users) // SELECT * FROM users where age >10;
fmt.Println(result,RowsAffrcted // 返回找到的记录数,相当于Len(users)
fmt.Println(result.Error) //returns error
//IN SELECT * FROM users WHERE name IN (jinzhu',jinzhu2');
db.Where( query:"name IN ?",[]string{"jinzhu""jinzhu 2"}).Find(&users)
//LIKE SELECT * FROM users WHERE nome LIKE %jin%':
db.Where( query:"name LIKE ?",args...: "%jin%").Find(&users)
//AND SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
db.Where(query: "name = ? AND age >= ?", args...: "jinzhu","22").Find(&users)
//SELECT * FROM users WHERE name = "jinzhu";
db.Where(&User{Name:"jinzhu",Age: 0}).Find(&users)
//SELECT * FROM users WHERE name = “jinzhu" AND age = 0
db.Where(map[string]interface{}{"Name": "jinzhu","Age": 0}).Find(&users)
查询第一条名称为 "Apple" 的产品数据
方法一:第一个参数是一个模型结构体的指针,第二个参数是查询条件,第三个参数是条件的值
var product Product
db.First(&product, "name = ?", "Apple")
方法二:使用链式查询来实现这个操作
var product Product
db.Where("name = ?", "Apple").First(&product)
在链式调用的时候,需要注意,如果查询不到符合条件的数据,First 方法将不会返回错误,而是返回一个空结构体。所以需要在使用时自己判断是否查询到数据。
var product Product
err := db.Where("name = ?", "Apple").First(&product).Error
if err != nil {
fmt.Println(err)
}
如果想要在查询不到数据时返回错误,可以使用 FirstOrInit 和 FirstOrCreate 方法,它们在查询不到数据时会返回错误。在使用 First 方法时, 如果传入的结构体中有组合索引,但没有使用 Where 函数来限制组合索引的值会触发。
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type User struct {
ID int
Name string
}
func main() {
db, err := gorm.Open("mysql", "root:password@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer db.Close()
// 查询第一条ID为1的用户
var user User
db.First(&user, 1)
fmt.Println(user)
}
这里我们使用了GORM的First方法来查询第一条ID为1的用户。需要注意的是,如果查询不到符合条件的记录,First方法会返回ecordNotFound错误。使用时需要注意,如果传递的参数是结构体指针,gorm会将查询结果赋值到结构体上,而传递结构体则不会有赋值操作。另外,在使用First方法查询时,如果没有符合条件的记录,返回的错误是RecordNotFound,需要在业务代码中特判这个错误。
2.4.7 Find
Find 方法用来查询符合条件的记录,并以切片的形式返回。
假设有一个名为users的表,你需要查询所有年龄大于30岁的用户。在下面的代码中,我们首先打开了一个数据库链接,然后定义了一个User结构体,用来存储查询到的用户信息。接着我们使用Where方法限定查询条件,并调用Find方法查询数据。Find方法的第一个参数是一个结构体切片的指针,用来存储查询到的记录。最后我们打印出查询到的用户信息。
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type User struct {
ID int
Name string
Age int
}
func main() {
db, err := gorm.Open("mysql", "root:password@/dbname?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
var users []User
db.Where("age > ?", 30).Find(&users)
fmt.Println(users)
}
注意:如果没有符合条件的记录,Find方法会返回一个空的切片。
2.4.8 First 和 Find
First方法和Find方法都是用来查询记录的,但是它们的用法和返回结果有所不同。First方法会查询并返回第一条符合条件的记录。它的返回值是一个结构体,如果没有符合条件的记录,会返回RecordNotFound错误。Find方法会查询所有符合条件的记录,并将它们以切片的形式返回。它的返回值是一个结构体切片,如果没有符合条件的记录,会返回一个空的切片。
举个例子,如果你要查询一个表中的第一条记录,可以使用First方法;如果你要查询一个表中所有符合条件的记录,可以使用Find方法。
总的来说,如果你只需要查询一条记录,使用First方法会更加高效,因为它只会查询一条记录;如果你需要查询多条记录,使用Find方法会更加方便。
2.4.9 更新数据
Gorm 提供了 Save 、 Updates 和 Update 方法用来更新数据。
Save方法更新所有字段,如果没有主键或者自增字段会新增一条记录。Updates方法用来更新指定字段。Update方法用来更新指定字段并且指定更新条件。
假设有一个名为users的表,你需要更新id为1的用户的姓名为 "John Doe"。在下面的代码中,我们首先打开了一个数据库链接,然后定义了一个User结构体,用来存储查询到的用户信息。接着我们使用First方法查询id 为1的用户,并更新用户的姓名。最后我们使用Save方法更新数据。
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
type User struct {
ID int
Name string
Age int
}
func main() {
db, err := gorm.Open("mysql", "root:password@/dbname?charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
var user User
db.First(&user, 1) // find user with id 1
user.Name = "John Doe"
db.Save(&user)
fmt.Println(user)
}
如果只需要更新一些字段,可以使用 Updates 方法,例如:
db.Model(&user).Updates(map[string]interface{}{"name": "John Doe", "age": 25})
2.4.9.1 Gorm 更新数据有两种方式
1.使用 Update 方法:
db.Model(&user).Update("name", "new_name")
这种方式可以对指定字段进行更新,而不会影响其他字段的值。 2.使用 Save 方法:
user.Name = "new_name"
db.Save(&user)
这种方式会更新整个结构体。
在使用这些方法之前,需要确保记录已经在数据库中存在。如果需要在更新之前判断记录是否存在,可以使用 FirstOrCreate 方法。
注意:在使用 Save 方法时,如果结构体中有没有被赋值的字段,那么这些字段可能会被赋上空值,所以需要特别小心。在更新数据时,Gorm 会自动跟踪更新时间,如果更新时间字段名为 updated_at,则自动更新,如果需要自定义字段名,需要手动设置。
2.4.10 删除
Gorm 提供了 Delete 方法来删除数据。使用 Delete 方法可以删除一条记录,方法的参数是要删除的记录的指针。这个方法会将这条记录从数据库中删除,删除之后该记录就不再是有效的
db.Delete(&user)
如果要删除多条记录,可以使用 Where 方法限制删除范围。这个方法会删除年龄大于20岁的所有用户。
db.Where("age > ?", 20).Delete(User{})
注意,在删除数据时,Gorm 不会自动跟踪删除时间,如果需要记录删除时间,需要手动设置。
2.4.11 物理删除和软删除
物理删除和软删除是 Gorm 提供的两种不同的删除方式。
-
- 物理删除就是直接从数据库中删除记录,不管有没有其他业务逻辑的依赖,这种方式删除的数据不可恢复,需要谨慎使用。
-
- 软删除是通过在数据表中添加一个删除标记来标记记录被删除,这种方式删除的数据可以恢复,适用于不需要永久删除数据的场景。
Gorm 提供了一些辅助方法来支持软删除,比如 DeletedAt 字段,在使用 Delete 方法删除数据时会自动设置这个字段,可以通过 Unscoped 方法来忽略这个字段来进行物理删除。Gorm默认不支持软删除,需要手动支持。Gorm 提供了 gorm.DeletedAt 用于帮助用户实现软删除。
package main
import (
"fmt"
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type User struct {
ID uint `gorm:"primary_key"`
Name string
Age uint
DeletedAt *time.Time `sql:"index"` // 定义软删除字段
}
func main() {
db, err := gorm.Open("sqlite3", "test.db")
if err != nil {
fmt.Println(err)
return
}
defer db.Close()
// 开启软删除
db.AutoMigrate(&User{}).Delete(&User{}, "deleted_at is not null")
db.Callback().Delete().Add("gorm:delete", func(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedAt")
if !scope.Search.Unscoped && hasDeletedOnField {
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deletedOnField.DBName),
scope.AddToVars(time.Now()),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
} else {
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
})
// 创建用户
user := User{Name: "John", Age: 20}
db.Create(&user)
// 软删除用户
db.Delete(&user)
// 查询已被软删除的用户
var deletedUser User
db.Unscoped().First(&deletedUser, "name = ?", "John")
fmt.Printf("Deleted user: %+v\n", deletedUser)
}
2.4.12 事务
Gorm支持多种方式来管理数据库的事务。一种常用的方式是使用db.Begin()来开启一个事务,然后使用tx.Commit()或tx.Rollback()来结束事务。
在GORM中,事务是通过DB对象的Begin()方法来开始一个事务,该方法返回一个TX对象。然后你可以在TX对象上执行数据库操作,最后调用Commit()或Rollback()来结束事务。
tx := db.Begin()
// 注意在后面从这里开始,你应该使用 `tx` 而不是 `db`
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Error; err != nil {
panic(err)
}
// 执行数据库操作
if err := tx.Commit().Error; err != nil {
panic(err)
}
或者使用 db.Transaction(func(tx *gorm.DB) error{}) 来进行事务操作
如果需要在一组数据库操作之间开始和结束事务,可以使用db.Transaction(func(tx *gorm.DB) error{}) 来进行事务操作
tansaction 方法用于自动提交事务,避免用户漏写 Commit、Rollbcak
err := db.Transaction(func(tx *gorm.DB) error {
// 执行数据库操作
return nil
})
注意:如果返回值为nil,事务会提交,如果返回值非nil,事务会回滚。
2.4.13 GORM Hook
GORM Hook是GORM中的一种事件钩子机制,它允许在对数据库进行操作时,在特定的时间点执行额外的代码。GORM Hook提供了多种事件类型,如创建、更新、查询和删除等。
可以使用db.Callback()来定义Hook,如下所示:
package main
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open("sqlite3", "test.db")
if err != nil {
panic("failed to connect database")
}
defer db.Close()
db.AutoMigrate(&Product{})
// 定义钩子
db.Callback().Create().Before("gorm:begin_transaction").
Register("gorm:before_create", func(scope *gorm.Scope) {
fmt.Println("在创建之前执行的代码")
})
db.Callback().Create().After("gorm:commit_or_rollback_transaction").
Register("gorm:after_create", func(scope *gorm.Scope) {
fmt.Println("在创建之后执行的代码")
})
// 创建一个Product实例并保存
db.Create(&Product{Code: "L1212", Price: 1000})
}
上面这个例子,当数据库创建操作时会触发gorm:before_create事件,并执行函数中的代码。可以使用如下的事件类型:
Before/After("gorm:create")Before/After("gorm:query")Before/After("gorm:update")Before/After("gorm:delete")
通过这些事件类型,可以在对数据库进行操作时,对数据进行预处理或后处理,也可以在数据库操作失败时进行错误处理。
2.4.14 Gorm 性能提高
GORM是一个ORM库,它会在运行时执行很多操作,因此其运行性能可能不如直接使用原生SQL。但是,还是有一些方法可以提高GORM的性能。
- 使用缓存: GORM支持对数据库的缓存,如果你的应用程序需要频繁读取数据库中的同一数据,可以考虑使用缓存来提高性能。
- 尽量减少查询次数: GORM在查询数据库时会执行很多操作,如果你可以通过减少查询次数来提高性能,就应该尽量这样做。
- 尽量减少使用高级功能: GORM支持很多高级功能,如果你不需要这些功能,可以考虑不使用它们,以提高性能。
- 尽量使用批量操作: GORM支持对多个对象批量进行操作,如果你需要对多个对象进行相同的操作,可以考虑使用批量操作来提高性能。
- 数据库连接池: GORM默认使用数据库连接池来管理数据库连接,但是默认的连接池大小可能不够,可以通过调整连接池大小来提高性能。
db,err := gorm.0pen(mysgl.Open( dsn: "username:password@tcp(localhost:9910)/database?charset=utf8"),
&grom.Config{
SkipDefaultTransaction: true,// 关闭默认事务
PrepareStmt: true},// 缓存预编译语句
)
if err != nil {
panic( v:"failed to connect database")
}
2.4.15 缓存预编译
预编译是一种将 SQL 语句编译为数据库可以识别的语句的过程。这种编译过程可以提高查询性能,因为数据库不需要在每次执行查询时都重新编译 SQL 语句。GORM 中可以使用预编译语句来提高查询性能。使用预编译语句的方法是将 SQL 语句传递给 gorm.DB.Exec 或 gorm.DB.Query,并将需要传递给 SQL 语句的参数作为第二个参数传递。
案例如下, 第一个参数是预编译的 SQL 语句,第二个参数是要传递给 SQL 语句的参数。
db.Exec("INSERT INTO users (name, age) VALUES (?, ?)", "John Doe", 30)
缓存预编译是一种优化查询性能的方法,它可以将预编译过的 SQL 语句存储在缓存中,以便在需要执行相同的查询时可以重用已编译的语句。这可以减少数据库编译 SQL 的开销,提高性能。strict=true 参数告诉 GORM 在每次操作时都启用事务,cache=true 参数告诉 GORM 缓存预编译语句。
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local&strict=true&cache=true")
使用缓存预编译语句可以显著提高查询性能,特别是对于执行相同的查询的情况。
2.4.16 关闭默认事务
GORM 默认在每次数据库操作时都会启用事务,但是有时候我们不需要这样.在 GORM 中关闭默认事务可以使用 gorm.Open 方法打开数据库连接时的参数。
db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local&autocommit=true")
其中,autocommit=true 参数告诉 GORM 在每次操作时不启用事务。如果不需要事务,这样做可以提高性能。
注意:关闭事务后需要自己手动管理事务,如果需要事务支持,请不要关闭默认事务。
2.4.17GORM生态
| 扩展名称 | 描述 | GitHub地址 |
|---|---|---|
| GORM 代码生成工具 | 可以根据数据库表结构自动生成 Go 代码,帮助使用 GORM 的用户更快速地开发 | github.com/go-gorm/gen |
| GORM 分片库方案 | 对 GORM 进行的分片库扩展,可以帮助用户管理分片数据库 | github.com/go-gorm/sha… |
| GORM 手动索引 | 帮助用户在 GORM 中手动创建索引 | github.com/go-gorm/hin… |
| GORM 乐观锁 | 对 GORM 进行的乐观锁扩展,可以帮助用户处理并发冲突 | github.com/go-gorm/opt… |
| GORM 读写分离 | 对 GORM 进行的读写分离扩展,可以帮助用户优化数据库性能 | github.com/go-gorm/dbr… |
| GORM OpenTelemetry 扩展 | 对 GORM 进行的 OpenTelemetry 扩展,可以帮助用户对 GORM 的性能进行监控和追踪 | github.com/go-gorm/ope… |