Go框架三件套(Web/RPC/GORM)二 | 青训营

92 阅读13分钟

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 提供的两种不同的删除方式。
    1. 物理删除就是直接从数据库中删除记录,不管有没有其他业务逻辑的依赖,这种方式删除的数据不可恢复,需要谨慎使用。
    1. 软删除是通过在数据表中添加一个删除标记来标记记录被删除,这种方式删除的数据可以恢复,适用于不需要永久删除数据的场景。
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…