OGORM 新手入门笔记

9 阅读7分钟

概要

就是go语言的一个对于数据库操作的包 让你更加方便的操作数据库 没了

初始化

plain go get -u gorm.io/gorm go get -u gorm.io/driver/sqlite

前情提要

约定
主键:GORM 使用一个名为ID 的字段作为每个模型的默认主键。

表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。比如说User 会变成users。GormUserName变成 gorm_user_names

列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。

时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。

概念Go 结构体/字段默认数据库映射规则说明
表名type User structusers结构体名转 小写 + 复数 (User -> users)。
主键IDid字段名为 ID (大小写不限) 默认为主键。
字段名Name, Agename, age驼峰命名 (CamelCase) 自动转为 蛇形命名 (snake_case)。
时间戳CreatedAtcreated_at特定字段名会被识别为自动管理的时间戳。

嵌套问题

type User struct {
    ID    uint `gorm:"primaryKey"`
    Name  string
    Email string
    Password
}
type Password struct {
    pas  int
    salt string
}

像这样的 一个结构体嵌套在另一个结构体中的 这种格式 只需要如上写法 就可以额 实现嵌套 等价于

type User struct {
    ID    uint `gorm:"primaryKey"`
    Name  string
    Email string
    pas  int
    salt string
}

方法二

对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID    int64
  Name  string
  Email string
  Upvotes  int32
}
并且,您可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀,例如:

type Blog struct {
  ID      int
  Author  Author `gorm:"embedded;embeddedPrefix:author_"`
  Upvotes int32
}
// 等效于
type Blog struct {
  ID          int64
  AuthorName string
  AuthorEmail string
  Upvotes     int32
}

链接

不连接怎么使用呢(笑


func main() {
	dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	log.Println("connected to database,", db)
}
 账号root 密码12345678

小demo 关于插入一个user

package main

import (
	"context"
	"log"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	Name string
	Age  int
}

func main() {
	dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	log.Println("connected to database,", db)
	user := User{Name: "Jinzhu", Age: 18}
	//Create a single record

    这个就是只是插入 而不知道结果
	ctx := context.Background()
	err = gorm.G[User](db).Create(ctx, &user) // pass pointer of data to Create

	// 这个是可以通过result 获取到影响行数
	result := gorm.WithResult()
	err = gorm.G[User](db, result).Create(ctx, &user)
	log.Println("user ID:", user.ID) // returns inserted data's primary key
	// log.Println("result error:", result.Error)                // returns error
	log.Println("result rows affected:", result.RowsAffected) // returns inserted records count



    

}

这种又长又难记的是通用api

通用 API (Generics API / 泛型 API)

是 GORM 为了拥抱 Go 语言新特性(Go 1.18 引入泛型)而推出的现代化写法。这里的“通用”翻译自 Generics(泛型),意指它可以通用地处理任何类型,但同时保持类型安全。

  • 核心特征
    • 显式指定类型:必须使用 <font style="color:rgb(6, 10, 38);">gorm.G[User]</font> 明确告诉编译器我们要操作 <font style="color:rgb(6, 10, 38);">User</font> 表。
    • 直接返回 error<font style="color:rgb(6, 10, 38);">Create</font> 方法直接返回 <font style="color:rgb(6, 10, 38);">error</font>,更符合 Go 语言的标准习惯 (<font style="color:rgb(6, 10, 38);">if err != nil</font>)。
    • 显式 Context<font style="color:rgb(6, 10, 38);">ctx</font><font style="color:rgb(6, 10, 38);">Create</font> 函数的第一个必传参数,强制你处理超时和取消逻辑。
    • 可选 Result:如果你需要 <font style="color:rgb(6, 10, 38);">RowsAffected</font>,需要额外创建一个 <font style="color:rgb(6, 10, 38);">result</font> 对象传进去(如你文档所示),否则默认只关心成功与否。
  • 代码样子go编辑
// 必须写 [User],明确类型
err := gorm.G[User](db).Create(ctx, &user)

// 直接判断 err
if err != nil { ... }

// 如果需要行数,才用 result
// result := gorm.WithResult()
// err := gorm.G[User](db, result).Create(ctx, &user)
  • 优点
    • 类型安全:编译器能帮你检查类型错误(比如把 <font style="color:rgb(6, 10, 38);">Product</font> 传给 <font style="color:rgb(6, 10, 38);">User</font> 的操作)。
    • IDE 提示更准:因为类型明确,自动补全更智能。
    • Context 规范:强制传递上下文,适合高并发 Web 服务。
    • 无需断言:读取数据时直接得到具体类型,不需要类型转换。
  • 缺点
    • 代码稍微长一点点(需要写 <font style="color:rgb(6, 10, 38);">[User]</font><font style="color:rgb(6, 10, 38);">ctx</font>)。
    • 需要较新的 GORM 版本 (v1.25+) 和 Go 版本 (1.18+)。
    • 网上的旧教程可能不涉及这种写法。

传统 API

  • 核心特征
    • 不指定类型:调用时不需要告诉 GORM 操作的是 <font style="color:rgb(6, 10, 38);">User</font> 还是 <font style="color:rgb(6, 10, 38);">Product</font>,它通过你传入的变量指针自动推断。
    • 返回对象<font style="color:rgb(6, 10, 38);">db.Create(&user)</font> 返回的是一个 <font style="color:rgb(6, 10, 38);">*gorm.DB</font> 对象(代码中的 <font style="color:rgb(6, 10, 38);">result</font>)。
    • 获取错误:需要访问 <font style="color:rgb(6, 10, 38);">result.Error</font>
    • Context 支持:比较隐式,通常需要先调用 <font style="color:rgb(6, 10, 38);">db.WithContext(ctx)</font>
// 不需要写 [User]
result := db.Create(&user) 

// 错误和结果都在 result 对象里
if result.Error != nil { ... }
count := result.RowsAffected
  • 优点
    • 代码简短,写起来快。
    • 兼容所有旧版本 GORM。
    • 社区资料最多。
  • 缺点
    • 类型安全性稍弱(依赖反射,编译器无法在写代码时检查你是否传错了结构体)。
    • Context 传递有时候容易忘记。

注意 在传统api中不能使用 结构体作为穿惨 二十使用指向结构体的指针

插入

	db.Select("name", "age").Create(&user)// 插入有的字段
// INSERT INTO `users` (`name`,`age`,) VALUES ("jinzhu", 18)
	db.Omit("name", "age").Create(&user)// 插入除有的字段
// INSERT INTO `users` (`location`,`birth`,) VALUES ("zhejiang", 18)

批量插入

var users = []User{{Name: "jinzhu1" }, {Name: "jinzhu2" }, {Name: "jinzhu3" }} 

db.CreateInBatches(users, 100 )//批次大小为 100 设置批次 可以不写
db.Create(&users)

for _, user := range users { 
  user.ID // 1,2,3
 }

根据 Map 创建

GORM支持通过 map[string]interface{} 与 []map[string]interface{}{}来创建记录。

db.Model(&User{}).Create(map[string]interface{}{
  "Name": "jinzhu", "Age": 18,
})
// batch insert from []map[string]interface{}{}
db.Model(&User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})
注意当使用map来创建时,钩子方法不会执行,关联不会被保存且不会回写主键。

钩子🪝函数

顾名思义 在执行前中后 的时候 会执行的函数

// 定义一个方法,绑定在 User 结构体上
// 方法名必须是 BeforeCreate,参数必须是 *gorm.DB,返回值必须有 error
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  
  // 【动作 1】在保存之前,自动生成一个 UUID
  // 当你调用 db.Create(&user) 时,哪怕你没给 UUID 赋值,
  // GORM 会在写入数据库前,先运行这行代码,把 UUID 填好。
  u.UUID = uuid.New()

  // 【动作 2】在保存之前,进行业务逻辑校验
  if u.Role == "admin" {
      // 如果角色是 admin,返回一个错误
      // 一旦返回错误,GORM 会立即停止!
      // 数据【不会】被写入数据库,整个 Create 操作失败。
      return errors.New("invalid role")
  }
  
  // 如果没有问题,返回 nil (err 默认为 nil),GORM 继续执行插入操作
  return
}

这个钩子是如何工作的?(生命周期)

假设你在 <font style="color:rgb(6, 10, 38);">main</font> 函数里写了这样一行代码:

user := User{Name: "Jinzhu", Role: "admin"}
err := db.Create(&user)

没有钩子时:

  1. GORM 接收 <font style="color:rgb(6, 10, 38);">user</font>
  2. GORM 生成 <font style="color:rgb(6, 10, 38);">INSERT INTO users ...</font> SQL。
  3. 发送给数据库。
  4. 结束。

有了**** **<font style="color:rgb(6, 10, 38);">BeforeCreate</font>** ****钩子后:

  1. GORM 接收 <font style="color:rgb(6, 10, 38);">user</font>
  2. ⏸️ GORM 暂停。它发现 <font style="color:rgb(6, 10, 38);">User</font> 结构体有一个叫 <font style="color:rgb(6, 10, 38);">BeforeCreate</font> 的方法。
  3. 🏃 GORM 自动执行你的**** **<font style="color:rgb(6, 10, 38);">BeforeCreate</font>** ****方法
    • 它把 <font style="color:rgb(6, 10, 38);">u.UUID</font> 填上了新值。
    • 它检查 <font style="color:rgb(6, 10, 38);">u.Role</font>,发现是 <font style="color:rgb(6, 10, 38);">"admin"</font>
    • 它执行 <font style="color:rgb(6, 10, 38);">return errors.New(...)</font>
  4. ⚠️ GORM 检测到错误:因为钩子返回了 error,GORM 决定中止后续操作。
  5. 不发送 SQL:数据没有写入数据库。
  6. 🔙 返回错误:你的 <font style="color:rgb(6, 10, 38);">db.Create(&user)</font> 收到那个 "invalid role" 错误。

查询

// 获取第一条记录(主键升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录,没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录(主键降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // 返回找到的记录数
result.Error        // returns error or nil

// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)

加上条件

String条件

比较low 有点像原生的slq语句

// Get first matched record
db.Where("name = ?", "jinzhu").First(&user)
// SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;

// Get all matched records
db.Where("name <> ?", "jinzhu").Find(&users)
// SELECT * FROM users WHERE name <> 'jinzhu';
这边就展示这两个 因为大差不差 就是 占位符+原生sql那一套
struct/map条件
// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&users)
// SELECT * FROM users WHERE id IN (20, 21, 22);

使用结构体 看起来比较nb一点

选择条件

or 
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)

排序

db.Order("age desc, name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

// Multiple orders
db.Order("age desc").Order("name").Find(&users)
// SELECT * FROM users ORDER BY age desc, name;

Limit & offset

	db.Limit(10).Find(&user).Limit(-1).Find(&user)
	db.Offset(10).Find(&user)

limit(-1) 是为了消除limit条件的 在链式查询中

Distinct

db.Distinct("name", "age").Order("name, age desc").Find(&results)

Scan

这是查询以后 将结果返还给 结构体

var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)