Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

go-gorm

概述

记录一下关于gorm相关的资料。使用gorm是比较普遍的数据库操作方式。其实资料很容易,这里记录一下,如果用起来的时候,将会非常快速上手。

现在chatGPT出来了,改变太多了,这种文章只能当作自己的草稿纸。

连接 mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"

"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gen"
"gorm.io/gorm"
)

func main() {
viper.SetConfigFile("config.json")
viper.AddConfigPath(".")
if err := viper.ReadInConfig(); err != nil {
panic(fmt.Sprintf("Failed to load config: %s", err))
}
logrus.SetFormatter(&logrus.TextFormatter{})

dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
viper.GetString("mysqldb.user"),
viper.GetString("mysqldb.password"),
viper.GetString("mysqldb.host"),
viper.GetInt("mysqldb.port"),
viper.GetString("mysqldb.database"),
)
g := gen.NewGenerator(gen.Config{
OutPath: "./query",
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
})

gormdb, _ := gorm.Open(mysql.Open(dsn))
g.UseDB(gormdb)

g.ApplyBasic(
g.GenerateModel("t_football_match"),
g.GenerateModel("t_ball_game"),
g.GenerateModel("t_basketball_match"),
g.GenerateModel("t_beidan_match"),
g.GenerateModel("t_racing_ball"),
)
g.Execute()
}

创建

upsert

这个在用于更新字段很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import "gorm.io/gorm/clause"

// Do nothing on conflict
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// Update columns to default value on `id` conflict
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET ***; SQL Server
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// Use SQL expression
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"count": gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count`=GREATEST(count, VALUES(count));

// Update columns to new value on `id` conflict
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// MERGE INTO "users" USING *** WHEN NOT MATCHED THEN INSERT *** WHEN MATCHED THEN UPDATE SET "name"="excluded"."name"; SQL Server
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age"; PostgreSQL
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age); MySQL

// Update all columns to new value on conflict except primary keys and those columns having default values from sql func
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL

更新

这里支持某基础上修改多少。

1
2
3
4
5
6
7
8
9
10
11
12
// product's ID is `3`
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;

db.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)})
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;

db.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;

db.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;

group方式读取指定某个数据

  1. 有一个表保存了很多person数据
  2. person数据里面包含 name, age, sex, address
  3. 只想查询一下 age

常见问题

处理空字符串

我发现两次这个问题了,在使用 gorm 的时候,string 是有可能为空的,如果没有小心处理将会造成 null 和 blank 混淆。这个时候只能使用下面这段代码来处理。防止出现问题。

1
2
"database/sql"
sqlString := sql.NullString{String: input, Valid: len(input)>0}

查询不出来问题

数据量少的时候,不会出现问题,但是数据量之后就会出现问题了。其实还是习惯问题。

搞错了事情:

  1. 每个record对象都需要重新创建,(否则会造成各种问题;数据量小的时候不会出问题,数据量大就会随机出问题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

var record models.Home
err := tx.Model(&models.Home{}).
Where("id = ?", id).
Where("address = ?", address).
First(&record).Error

if err == nil {
return &record, nil
}
rawErr := err

// 1. 函数域内,不能重复使用 models
var record models.Home
err = tx.Model(&models.Home{}).
Where("address = ?", address).
First(&byaddr_record).Error

密码无需做escape

密码无需做escape就能用。
在使用 GORM(Go的ORM库)时,通常不需要手动进行SQL转义,因为GORM会自动处理大多数的SQL注入问题。然而,如果你确实需要手动对一个字符串进行SQL转义,可以使用GORM提供的一些辅助函数。

GORM的DB.Raw方法允许你执行原生的SQL查询,同时可以使用db.Conn的方法对字符串进行SQL转义。

以下是一个示例,展示如何对字符串进行SQL转义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"log"

"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func main() {
// 初始化数据库连接
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database")
}

// 定义需要转义的字符串
rawString := "O'Reilly"

// 使用 GORM 的 DB.Conn 方法进行转义
escapedString := db.Conn(func(conn *gorm.DB) error {
escapedString := conn.Dialector.Explain("?", rawString)
fmt.Println("Escaped String:", escapedString)
return nil
})

if escapedString != nil {
log.Fatal("failed to escape string")
}
}

在这个示例中,conn.Dialector.Explain 方法用来生成包含转义字符串的SQL查询。这个方法接收一个格式化字符串和参数,返回一个包含转义字符串的完整查询。

如果你只是需要转义字符串,而不需要执行查询,可以使用fmt.Sprintf结合conn.Dialector.Explain来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"log"

"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func main() {
// 初始化数据库连接
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database")
}

// 定义需要转义的字符串
rawString := "O'Reilly"

// 使用 GORM 的 DB.Conn 方法进行转义
var escapedString string
db.Conn(func(conn *gorm.DB) error {
escapedString = fmt.Sprintf("%s", conn.Dialector.Explain("?", rawString))
return nil
})

// 打印转义后的字符串
fmt.Println("Escaped String:", escapedString)
}

以上代码会正确转义字符串,使其在SQL查询中安全可用。如果你有更多特定的需求或遇到其他问题,请告诉我!

多字段unique

1
2
3
4
5
type Something struct {
gorm.Model
First string `gorm:"index:idx_name,unique"`
Second string `gorm:"index:idx_name,unique"`
}

需要调查一下

1
2
3
unreadable: invalid interface type

未查询到记录返回该错误

软删除

Unscoped

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// Unscoped disables the global scope of soft deletion in a query.
// By default, GORM uses soft deletion, marking records as "deleted"
// by setting a timestamp on a specific field (e.g., `deleted_at`).
// Unscoped allows queries to include records marked as deleted,
// overriding the soft deletion behavior.
// Example:
// var users []User
// db.Unscoped().Find(&users)
// // Retrieves all users, including deleted ones.
func (db *DB) Unscoped() (tx *DB) {
tx = db.getInstance()
tx.Statement.Unscoped = true
return
}

在 GORM 中,默认情况下,模型的删除操作是软删除,即不会真正从数据库中删除记录,而是设置一个 DeletedAt 字段来标记记录被删除。如果你需要真正删除记录而不是软删除,可以通过以下几种方式来实现。

  1. 禁用软删除功能

如果你不想使用软删除功能,可以直接禁用它,这样 Delete 操作就会真正从数据库中删除记录。

假设你有一个模型:

type User struct {
ID uint
Name string
Age int
}

你可以直接使用 db.Delete() 来执行真正的删除操作:

var user User
db.Where(“id = ?”, 1).Delete(&user)

这将从数据库中真正删除该记录,而不是软删除。

  1. 忽略软删除字段(如果模型带有 gorm.DeletedAt)

如果你的模型带有 gorm.DeletedAt 字段,且默认使用了软删除,你可以通过以下方式忽略软删除字段,实现真实删除。

例如,带有软删除字段的模型:

type User struct {
ID uint
Name string
Age int
DeletedAt gorm.DeletedAt gorm:"index"
}

在删除时通过 Unscoped() 方法来忽略软删除逻辑,实现真实删除:

var user User
db.Unscoped().Where(“id = ?”, 1).Delete(&user)

Unscoped() 方法会忽略软删除特性,直接从数据库中删除记录。

  1. 使用 Exec 执行原生 SQL 语句

如果你想完全避免 GORM 的删除逻辑,直接使用原生 SQL 来执行删除操作:

db.Exec(“DELETE FROM users WHERE id = ?”, 1)

这种方式不经过 GORM 的任何处理,直接执行 SQL 语句,完全由你控制删除行为。

总结

•    默认删除:db.Delete(&user),会执行软删除(如果模型有 DeletedAt 字段)。
•    真实删除:db.Unscoped().Delete(&user),忽略软删除,真正从数据库中删除。
•    原生 SQL 删除:db.Exec("DELETE FROM users WHERE id = ?", id),直接执行 SQL 删除操作。

时间为空的情况

Data truncation: Incorrect datetime value: ‘0000-00-00 00:00:00’ for column ‘expire_time’ at row 1

需要对字段定义成这样:

1
2
3
type YourModel struct {
ExpireTime *time.Time `gorm:"default:NULL"` // 使用指针,以允许 NULL 值
}

使用 COALESCE 防止报错

1
2
3
tx := dbinit.Gormdb.Model(&model.TbUserFlow{}).
Where("user_id = ? status = ?", userId, Init).
Select("COALESCE(SUM(changed),0)").Scan(&total)

在 GORM 中,你可以使用 Updates 方法来进行批量更新,但默认情况下,Updates 只能一次性更新符合条件的多条记录为相同的值。如果你需要为每条记录更新不同的值,则需要结合其他方法。

  1. 使用 Updates 方法批量更新相同的字段值

如果你想更新多条记录,但每条记录的更新字段值相同,可以使用如下代码:

db.Model(&model.User{}).Where(“id IN ?”, []int{1, 2, 3}).Updates(map[string]interface{}{
“name”: “New Name”,
“age”: 30,
})

上面代码会将 id 为 1、2、3 的用户的 name 字段更新为 “New Name”,age 更新为 30。

  1. 批量更新不同的字段值

GORM 本身没有直接支持为每条记录批量更新不同的字段值的功能。为了实现批量更新不同字段值的需求,可以通过手动构造批量更新的 SQL 语句或使用事务来逐条更新。

方法1:构造原生 SQL 语句

可以通过 Raw SQL 实现批量更新不同值的需求。例如:

updates := []struct {
ID int
Name string
Age int
}{
{ID: 1, Name: “Alice”, Age: 25},
{ID: 2, Name: “Bob”, Age: 30},
{ID: 3, Name: “Charlie”, Age: 35},
}

// 构造 SQL
sql := “UPDATE users SET name = CASE id”
ids := []int{}
for , u := range updates {
sql += fmt.Sprintf(“ WHEN %d THEN ‘%s’”, u.ID, u.Name)
ids = append(ids, u.ID)
}
sql += “ END, age = CASE id”
for
, u := range updates {
sql += fmt.Sprintf(“ WHEN %d THEN %d”, u.ID, u.Age)
}
sql += “ END WHERE id IN ?”

// 执行 SQL
db.Exec(sql, ids)

这段代码会生成如下 SQL 语句,并执行批量更新:

UPDATE users SET name = CASE id
WHEN 1 THEN ‘Alice’
WHEN 2 THEN ‘Bob’
WHEN 3 THEN ‘Charlie’
END, age = CASE id
WHEN 1 THEN 25
WHEN 2 THEN 30
WHEN 3 THEN 35
END WHERE id IN (1, 2, 3);

方法2:使用事务进行逐条更新

如果你不想构造复杂的 SQL,可以使用事务逐条更新数据:

tx := db.Begin()

updates := []struct {
ID int
Name string
Age int
}{
{ID: 1, Name: “Alice”, Age: 25},
{ID: 2, Name: “Bob”, Age: 30},
{ID: 3, Name: “Charlie”, Age: 35},
}

for _, u := range updates {
tx.Model(&model.User{}).Where(“id = ?”, u.ID).Updates(map[string]interface{}{
“name”: u.Name,
“age”: u.Age,
})
}

tx.Commit()

这种方式虽然性能不如原生 SQL 高效,但代码逻辑较为简洁,并且事务可以保证数据一致性。

  1. 使用 Save 方法批量保存(插入或更新)

如果你的需求是根据主键批量插入或更新数据,你可以使用 Save 方法:

users := []model.User{
{ID: 1, Name: “Alice”, Age: 25},
{ID: 2, Name: “Bob”, Age: 30},
{ID: 3, Name: “Charlie”, Age: 35},
}

db.Save(&users)

GORM 会根据主键判断记录是否存在,如果存在则执行 UPDATE,否则执行 INSERT。

总结

•    如果要批量更新相同字段值,可以直接使用 GORM 的 Updates 方法。
•    如果要批量更新不同字段值,可以使用构造的原生 SQL 或使用事务逐条更新的方式。
•    如果你的需求是根据主键批量插入或更新,可以使用 Save 方法。

具体选择哪种方式取决于你的业务需求和性能考虑。

参考