DATABASE/SQL与GORM设计与实践 | 青训营笔记

236 阅读6分钟

前言

这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记,做笔记记录一下自己的学习过程。

此笔记主要内容如下:

  1. 理解database/sql
  2. GORM使用简介

1 理解database/sql

  • 基本用法
  • 设计原理
  • 基础概念

1.1 基础用法 - Quick Start

  • import driver实现 import "github.com/go-sql-driver/mysql"
  • 使用driver+DSN初始化DB连接 db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hello")
  • 执行一条SQL,通过rows取回返回的数据
  • 处理完毕,需要释放链接
    rows,err := db.Query("select id, name from users where id = ?", 1)
    if err != nil {
       // xxx
    }
    defer rows.Close()
  • 数据、错误处理
    var users []User
    for rows.Next() {
       var user User
       err := rows.Scan(&user.ID, &user.Name)
       if err != nil {
          // ...
       }
       users = append(users, user)
    }
    if rows.Err() != nil {
       // ...
    }

1.2 设计原理

  • database/sql采用极简接口设计原则,对上面应用程序提供标准API操作接口,对下层驱动暴露简单驱动接口,在database/sql包内部实现连接池的管理。 image.png
  • 这意味着支持不一样的数据库,只需要对数据库实现相同的连接接口,操作接口,然后把同样的一套操作接口暴露给应用程序就可以了。

1.3 基础概念

2 GORM的基础使用

  • 基础用法
  • Model定义
  • 惯例约定
  • 关联操作

2.1 基础用法

2.1.1 数据库连接

db, err := gorm.Open(
   mysql.Open("user:password@tcp(127.0.0.1:3306)/dbname"), &gorm.Config{
      NamingStrategy: schema.NamingStrategy{
         SingularTable: true,
      },
   })
if err != nil {
   panic("failed to connect database")
}
  • GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定,默认 NamingStrategy 也提供了SingularTable的选项可以使用单数表名。
  • 更多GORM命名策略配置可查看GORM配置文档

2.1.2 CRUD

  • Create
// 创建
user := User{Id: "nh100116", Name: "zzb"}
result := db.Create(&user)

fmt.Println(user.Id)               //返回主键 last insert id
fmt.Println(result.Error)          //返回 error
fmt.Println(result.RowsAffected)   //返回影响的行数,这里是插入多少行

// 批量创建
var users = []User{{Name: "zzb1"}, {Name: "zzb2"}, {Name: "zzb3"}}
result := db.Create(&users)
fmt.Println(result.RowsAffected)  //插入3行,这里输出是3
  • Query
// 读取
var users []User
result := db.Select("id", "name").Find(&users)
//带条件的查询
result := db.Select("id", "name").Where("id = ?", "nh100111").Find(&users)
fmt.Println(result.RowsAffected)  //查询到几行就输出几
  • Update
var user User
// 更新某个字段
result := db.Model(&user).Where("name = ?", "zzb1").Update("id", "nh100123")
result := db.Model(&user).Where("name = ?", "zzb2").UpdateColumn("id", "nh100133")

// 更新多个字段
result := db.Model(&user).Where("name = ?", "zzb1").Updates(User{Id: "nh100144", Name: "zzb11"})
result := db.Model(&user).Where("name = ?", "zzb2").Updates(map[string]interface{}{"Id": "nh100155", "Name": "zzb22"})

fmt.Println(result.RowsAffected)  //更新了几行就输出几
  • Delete
// 删除
result := db.Delete(&User{}, "name = ?", "zzb3") //批量删除所有符合条件的记录
fmt.Println(result.RowsAffected)  //删除几条记录就输出几
  • 小结
    • 可以用result.RowAffected来获取影响的数据库记录数
    • 可以用Select、Where等指定字段与条件
    • 具体更多GORM操作可查看GORM文档

2.2 模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成。

  • 嵌入结构体
type User struct {
   Id   string
   Name string
   gorm.Model
}
//等价于
type User struct {
   Id   string
   Name string
}
type User struct {
   Id string
   Name string
   ID        uint           `gorm:"primaryKey"`
   CreatedAt time.Time
   UpdatedAt time.Time
   DeletedAt gorm.DeletedAt `gorm:"index"`
}

2.3 惯例约定

  • 约定优于配置
    • 表名为struct name的snake_cases复数格式(结构体小写的复数格式)
    • 字段名为filed name的snake_case单数格式(结构体字段小写的单数格式)
    • ID/Id字段为主键,如果为数字,则为自增主键
    • CreatedAt字段,创建时,保存当前时间
    • UpdatedAt字段,创建、更新时,保存当前时间
    • gorm.DeletedAt字段,默认开启soft delete软删除模式
      • 如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。

2.4 关联介绍

  • User 拥有一个 Account (has one) ,拥有多个 Pets (has many) ,多个 Toys (多态 has many) 。
  • 属于某 Company (belongs to) 属于某 Manager (单表 belongs to) 管理 Team (单表 has many)
  • 会多种 Languages (many to many) 拥有很多 Friends (单表 many to many)
  • 并且他的 Pet 也有一个玩具 Toy (多态 has one)
type User struct {
   gorm.Model
   Name      string
   Account   Account
   Pets      []*Pet
   Toys      []Toy `gorm:"polymorphic:Owner"`
   CompanyID *int
   Company   Company
   ManagerID *uint
   Manager   *User
   Team      []User     `gorm:"foreignkey:ManagerID"`
   Languages  []Language `gorm:"many2many:UserSpeak;"`
   Friends   []*User    `gorm:"many2many:user_friends;"`
}

2.4.1 关联操作 - CRUD

结构体

type User struct {
   gorm.Model
   Name      string
   Languages []Language `gorm:"many2many:UserSpeak;"`
}
type Language struct {
   gorm.Model
   Name string
}
user := User{
   Name: "lyy",
   Languages: []Language{
      {Name: "ZH"},
      {Name: "EN"},
   },
}
//自动创建、更新记录
db.Create(&user)


var user User
db.First(&user)
//关联模式
langAssociation := db.Model(&user).Association("Languages")

//查找关联
var languages []Language
langAssociation.Find(&languages)
fmt.Println(languages)

//添加关联
//把德语添加到用户掌握的语言中
langAssociation.Append(&Language{Name: "DE"})

//替换关联
//把用户掌握的语言替换为汉语、德语
var languageZH Language
db.Where("id = ?", 1).First(&languageZH)
var languageDE Language
db.Where("id = ?", 5).First(&languageDE)
langAssociation.Replace([]Language{languageZH, languageDE})

//删除关联
//删除用户掌握的两种语言
langAssociation.Delete(languageEN, languageDE)

//清空关联
//删除用户所有掌握的语言,仅在关联表中删除,不删除语言、用户实体
langAssociation.Clear()

//关联计数
//返回用户所掌握的语言的数量
langAssociation.Count()

// 批量模式Append、Replace,参数的长度必须与数据的长度相同,否则会返回 error
// 例如:现在有三个 user,将 userA 到 user1 的 team,
// 将 userB 到 user2 的 team,将 userA、userB 和 userC 到 user3 的 team
var users = []User{user1, user2, user3}
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})

2.4.2 关联操作 - 级联删除

//方法1:使用Select实现级联删除,不依赖数据库约束及软删除
//删除user时,也删除user掌握的language
//在关系表中删除,也就是不删除language实体
var user User
db.First(&user)
db.Select("Language").Delete(&user)
//删除user时,也删除用户及其依赖的所有has one/many, many2many记录
db.Select(clause.Associations).Delete(&user)

//方法2:使用数据库约束自动删除
type User struct {
   ID uint
   Name string
   Account Account           `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
   CreditCards []CreditCard  `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
   Orders []Order            `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}

//需要使用GORM Migrate 数据库迁移数据库外键才行
db.AutoMigrate(&User{})
//如果未启用软删除,在删除User时会自动删除其依赖
db.Delete(&User{})

2.4.3 关联操作 - Preload / Joins 预加载

type User struct {
   gorm.Model
   Username string
   Orders   []Order
}

type Order struct {
   gorm.Model
   UserID uint
   Price  float64
}

// 查询用户的时候并找出其订单,个人信息 (1+1SQL)
db.Preload("Orders").Preload("Profile").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4); // 一对多
// SELECT * FROM profiles WHERE user_id IN (1,2,3,4); // 一对一

// 使用 Join SQL 加载 (单条 JOIN SQL)
// 注意 Join Preload 适用于一对一的关系
// Preload 在一个单独查询中加载关联数据。而 Join Preload 会使用 inner join 加载关联数据
db.Joins("Company").Joins("Manager").Joins("Account").First(&user, 1)
// 带条件的Join
db.Joins("Company", DB.Where(&Company{Alive: true})).Find(&users)

// 预加载全部关联 (只加载一级关联)
db.Preload(clause.Associations).Find(&users)

// 多级/嵌套 预加载
db.Preload("Orders.OrderItems.Product").Find(&users)
// 多级预加载 + 预加载全部一级关联
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users)

// 带条件的预加载
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4) AND state NOT IN ('cancelled');
db.Where("state = ?", "active").Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// SELECT * FROM users WHERE state = 'active';
// SELECT * FROM orders WHERE user_id IN (1,2) AND state NOT IN ('cancelled');