GORM 操作简介

2018-01-19 golang database

Object Relational Mapping, ORM 对象关系映射,用于将数据库中的数据与代码中的结构体进行映射,不同语言的实现方式略有区别,但是目的基本相同。

GoLang 中有多种映射库,这里简单介绍常见的 GORM 使用方式。

简介

ORM 实际上就是对数据库的操作进行封装,屏蔽数据库操作细节,从而简化开发,提高效率,GoLang 的 ORM 可以参考 gorm.io,其使用方法简单介绍如下。

GORM 倾向于使用约定而不是配置,默认使用 ID 作为主键,字段名的蛇型作为列名,结构体的蛇型复数作为表名,而且内部通过 gorm.Model 结构体提供了 CreatedAtUpdatedAt 字段跟踪创建、更新时间,通过 DeletedAt 作为逻辑删除时间。

例如结构体 UserInfo 实际转换为 user_infos,字段名UserName 将转换为 user_name,以此类推。

示例

如下是一个简单的示例,可以先手动创建表。

CREATE TABLE IF NOT EXISTS `users` (
    `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `name` char(64) NOT NULL COMMENT '用户名',
    `age` int NOT NULL COMMENT '用户的年龄',
    `gender` enum('no','male','female') DEFAULT 'no' COMMENT '性别'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO users(id, name, age, gender) VALUES(1, "andy", 18, "male");

CREATE TABLE IF NOT EXISTS `roles` (
    `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `name` char(64) NOT NULL COMMENT '角色名'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO roles(id, name) VALUES(1, "admin");

CREATE TABLE IF NOT EXISTS `map_user_role` (
    `user_id` int NOT NULL,
    `role_id` int NOT NULL,
    PRIMARY KEY(user_id, role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO map_user_role(user_id, role_id) VALUES(1, 1);

通过 GORM 执行 SQL 如下。

package main

import (
	"log"

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

type UserInfo struct {
	ID     int
	Name   string
	Age    int
	Gender string
}

func (*UserInfo) TableName() string {
	return "users"
}

func main() {
	dsn := "root:msandbox@tcp(127.0.0.1:8027)/test?charset=utf8&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: dblog.Default.LogMode(dblog.Info),
	})
	if err != nil {
		log.Printf("Open database failed, %v.", err)
		return
	}
	//db.Debug().AutoMigrate(&UserInfo{}) // 迁移Schema字段类型未指定

	// Create
	db.Create(&UserInfo{Name: "Andy", Age: 18, Gender: "male"})
    // 只写入指定字段,或者通过 Omit() 忽略字段
	db.Select("Name", "Age").Create(&UserInfo{Name: "Andy", Age: 18, Gender: "male"})

	// Read
	var user UserInfo
	db.First(&user, 1)                  // 根据主键查找
	db.First(&user, "name = ?", "Andy") // 通过索引查找

	// Update
	db.Model(&user).Update("Price", 20)                       // 将 Age 更新为 20
	db.Model(&user).Updates(&UserInfo{Name: "andy", Age: 20}) // 仅更新非零值字段
	db.Model(&user).Updates(map[string]interface{}{"Name": "andy", "Age": "20"})

	// Delete
	db.Delete(&UserInfo{}, 1) // 根据ID删除
}

其中 gorm.io/driver/mysql 是 MySQL 驱动,实际上就是 github.com/go-sql-driver/mysql 只是进行了重新命名。

gorm 用 tag 来标识 MySQL 里面的约束,创建索引只需要直接指定列即可,如果需要多列组合索引,直接让索引的名字相同即可。

常用技巧

创建表

可以通过 db.HasTable() 来判断表是否存在,其入参可以使用两种形式:A) 字符串;B) 模型的地址类型。其判断方式是直接查询 INFORMATION_SCHEMA.TABLES 表中的数据。

if ok := DB.HasTable("foos"); ok {
    t.Errorf("Table should not exist, but does")
}
if ok := DB.HasTable(&Foo{}); ok {
    t.Errorf("Table should not exist, but does")
}

定义模型时,必须指定字段的首字母为大写,否则无法创建字段,同时可以使用 gorm tag 进行制定,可以参考 Declaring Models,不过有些调试起来比较复杂,还是直接创建比较好。

db.CreateTable(&User{})

r1 := db.DropTable("Users")
r2 := db.DropTable(&User{})

默认创建的表名为复数形式,例如 User 创建后的表名为 users ,如果不想创建复数形式的表名,可以通过如下的语句设置。

db.SingularTable(true)

如果要自己定义,可以通过如下方式修改。

type UserInfo struct {} // 默认表名是user_infos

// 设置UserInfo的表名为users
func (UserInfo) TableName() string {
    return "users"
}

func (u UserInfo) TableName() string {
    if u.Role == "admin" {
        return "admin_users"
    } else {
        return "users"
    }
}

更新时间

gorm 提供了三个与时间相关的字段,会在操作时自动更新,包括了 CreatedAt UpdatedAt DeletedAt

----- 字段CreatedAt用于存储记录的创建时间
db.Create(&user) // 将会设置CreatedAt为当前时间
----- 要更改它的值, 需要使用Update
db.Model(&user).Update("CreatedAt", time.Now())

----- 字段UpdatedAt用于存储记录的修改时间
db.Save(&user)                           // 将会设置UpdatedAt为当前时间
db.Model(&user).Update("name", "jinzhu") // 将会设置UpdatedAt为当前时间

如果要使用不同的字段,可以在 tag 中配置 autoCreateTimeautoUpdateTime 标签,要保存时间戳而不是 time,只需简单地将 time.Time 修改为 int 即可。

type User struct {
    CreatedAt time.Time // Set to current time if it is zero on creating
    UpdatedAt int       // Set to current unix seconds on updating or if it is zero on creating
    Updated   int64 `gorm:"autoUpdateTime:nano"` // Use unix nano seconds as updating time
    Updated   int64 `gorm:"autoUpdateTime:milli"`// Use unix milli seconds as updating time
    Created   int64 `gorm:"autoCreateTime"`      // Use unix seconds as creating time
}

嵌入结构体

上述示例中嵌入结构体可以直接使用,可以增加 gorm:"embedded" 显示表示嵌入,或者通过 gorm:"embedded;embeddedPrefix:author_" 指定前缀。

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

连接池

使用的是标准库 database/sql 维护的连接池。

// 设置空闲连接池中连接的最大数量
db.SetMaxIdleConns(10)

// 设置打开数据库连接的最大数量
db.SetMaxOpenConns(100)

// 设置了连接可复用的最大时间
db.SetConnMaxLifetime(time.Hour)

高级技巧

Clause

GORM 内部会使用 SQL Builder 生成 SQL,而对于每个操作都会创建一个 *gorm.Statement 对象,所有的 GORM API 都是在为 Statement 添加或修改 Clause,最后根据这些 Clause 生成 SQL 语句。

例如,当执行 Limit 时会在 Statement 中添加以下 Clause 语句。

// Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
    tx = db.getInstance()
    tx.Statement.AddClause(clause.Limit{Limit: limit})
    return
}

然后在最后的 Callback 中构建最终的 SQL 语句,所以,可自定义 Clause 并与 GORM 一起使用。

不同数据库 Clause 可能会生成不同的 SQL 语句,例如:

db.Offset(10).Limit(5).Find(&users)
// SQL Server
SELECT * FROM "users" OFFSET 10 ROW FETCH NEXT 5 ROWS ONLY
// MySQL
SELECT * FROM `users` LIMIT 5 OFFSET 10

Upsert

MySQL 提供了 ON DUPLICATE KEY UPDATE 特有功能的支持,也就是如果 Insert 已经存在的记录,那么就执行 Update 操作,可以简化这一场景的实现逻辑,其它数据库也有类似的逻辑。

import "gorm.io/gorm/clause"

// 在冲突时,什么都不做
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)

// 在`id`冲突时,将列更新为默认值
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE ***; MySQL

// 使用SQL语句
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));

// 在`id`冲突时,将列更新为新值
db.Clauses(clause.OnConflict{
  Columns:   []clause.Column{{Name: "id"}},
  DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age=VALUES(age); MySQL

// 在冲突时,更新除主键以外的所有列到新值。
db.Clauses(clause.OnConflict{
  UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "age"="excluded"."age", ...;

详见 Gorm Upsert 中的介绍。

参考