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 删除操作。

参考