Go中Gorm的基本使用 | 青训营笔记

380 阅读10分钟

Gorm用法介绍

概述

GORM是Golang目前比较热门的数据库ORM操作库,对开发者也比较友好,使用非常方便简单,使用上主要就是把struct类型和数据库表记录进行映射,操作数据库的时候不需要直接手写Sql代码,这里主要介绍MySQL数据库。
GORM库github地址: github.com/go-gorm/gor…

GORM 约定

GORM 在使用有以下几点约定:

  • GORM 使用名为 ID 的字段作为主键
  • 如果没有 TableName 函数,使用结构体的蛇形复数作为表名
  • 字段名的蛇形作为列表
  • 使用 CreatedAt、UpdateAt 字段作为创建更新时间

库安装

go get -u github.com/jinzhu/gorm

数据库连接

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// UserInfo 用户信息
type UserInfo struct {
    ID uint
    Name string
    Gender string
    Hobby string
}

func main() {
    db, err := gorm.Open("mysql", "root:root@(127.0.0.1:3306)/db1?charset=utf8mb4&parseTime=True&loc=Local")
    if err!= nil{
        panic(err)
    }
    defer db.Close()

    // 自动迁移
    db.AutoMigrate(&UserInfo{})

    u1 := UserInfo{1, "枯藤", "男", "篮球"}
    u2 := UserInfo{2, "topgoer.com", "女", "足球"}
    // 创建记录
    db.Create(&u1)
    db.Create(&u2)
    // 查询
    var u = new(UserInfo)
    db.First(u)
    fmt.Printf("%#v\n", u)
    var uu UserInfo
    db.Find(&uu, "hobby=?", "足球")
    fmt.Printf("%#v\n", uu)
    // 更新
    db.Model(&u).Update("hobby", "双色球")
    // 删除
    db.Delete(&u)
}

连接比较简单,直接调用 gorm.Open 传入数据库地址即可 github.com/jinzhu/gorm/dialects/mysqlgolangmysql 驱动,实际上就是 github.com/go-sql-driver/mysql 作者这里为了好记,重新弄了个名字 这里我用的 mysql,实际上支持基本上所有主流的关系数据库,连接方式上略有不同

db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)

还可以使用 db.DB() 对象设置连接池信息

表定义

先来定义一个点赞表,这里面一条记录表示某个用户在某个时刻对某篇文章点了一个赞,用 ip + ua 来标识用户,title 标识文章标题

type Like struct {
    ID        int    `gorm:"primary_key"`
    Ip        string `gorm:"type:varchar(20);not null;index:ip_idx"`
    Ua        string `gorm:"type:varchar(256);not null;"`
    Title     string `gorm:"type:varchar(128);not null;index:title_idx"`
    Hash      uint64 `gorm:"unique_index:hash_idx;"`
    CreatedAt time.Time
}

gormtag 的方式来标识 mysql 里面的约束

创建索引只需要直接指定列即可,这里创建了两个索引,ip_idxtitle_idx;如果需要多列组合索引,直接让索引的名字相同即可;如果需要创建唯一索引,指定为 unique_index 即可

支持时间类型,直接使用 time.Time 即可

创建表

if !db.HasTable(&Like{}) {
    if err := db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8").CreateTable(&Like{}).Error; err != nil {
        panic(err)
    }
}

直接通过 db.CreateTable 就可以创建表了,非常方便,还可以通过 db.Set 设置一些额外的表属性

插入

like := &Like{
    Ip:        ip,
    Ua:        ua,
    Title:     title,
    Hash:      murmur3.Sum64([]byte(strings.Join([]string{ip, ua, title}, "-"))) >> 1,
    CreatedAt: time.Now(),
}

if err := db.Create(like).Error; err != nil {
    return err
}

先构造已给对象,直接调用 db.Create() 就可以插入一条记录了

删除

if err := db.Where(&Like{Hash: hash}).Delete(Like{}).Error; err != nil {
    return err
}

先用 db.Where() 构造查询条件,再调用 db.Delete() 就可以删除

查询

var count int
err := db.Model(&Like{}).Where(&Like{Ip: ip, Ua: ua, Title: title}).Count(&count).Error
if err != nil {
    return false, err
}

先用 db.Model() 选择一个表,再用 db.Where() 构造查询条件,后面可以使用 db.Count() 计算数量,如果要获取对象,可以使用 db.Find(&Likes) 或者只需要查一条记录 db.First(&Like)

修改

db.Model(&user).Update("name", "hello")
db.Model(&user).Updates(User{Name: "hello", Age: 18})
db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false}) // nothing update

我这个系统里面没有更新需求,这几个例子来自于官网,第一个是更新单条记录;第二个是更新整条记录,注意只有非空字段才会更新;第三个例子是不会更新的,在系统设计的时候要尽量避免这些空值有特殊的含义,如果一定要更新,可以使用第一种方式,设置单个值

错误处理

其实你已经看到了,这里基本上所有的函数都是链式的,全部都返回 db 对象,任何时候调用 db.Error 就能获取到错误信息,非常方便

事务

func CreateAnimals(db *gorm.DB) err {
    tx := db.Begin()
    if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
        tx.Rollback()
        return err
    }
    if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
        tx.Rollback()
        return err
    }
    tx.Commit()
    return nil
}

事务的处理也很简单,用 db.Begin() 声明开启事务,结束的时候调用 tx.Commit(),异常的时候调用 tx.Rollback()

其他

还可以使用如下方式设置日志输出级别以及改变日志输出地方

db.LogMode(true)
db.SetLogger(gorm.Logger{revel.TRACE})
db.SetLogger(log.New(os.Stdout, "\r\n", 0))

也支持普通的 sql,但是建议尽量不要使用


Gorm查询

查询

// 获取第一条记录,按主键排序
db.First(&user)
//// SELECT * FROM users ORDER BY id LIMIT 1;
// 获取最后一条记录,按主键排序
db.Last(&user)
//// SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 获取所有记录
db.Find(&users)
//// SELECT * FROM users;
// 使用主键获取记录
db.First(&user, 10)
//// SELECT * FROM users WHERE id = 10;

Where查询条件 (简单SQL)

// 获取第一个匹配记录
db.Where("name = ?", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name = 'jinzhu' limit 1;

// 获取所有匹配记录
db.Where("name = ?", "jinzhu").Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu';
db.Where("name <> ?", "jinzhu").Find(&users)

// IN
db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)

// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)

// Time
db.Where("updated_at > ?", lastWeek).Find(&users)

db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)

Where查询条件 (Struct & Map)

注意:当使用struct查询时,GORM将只查询那些具有值的字段

// Struct
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
//// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;

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

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

Not条件查询

db.Not("name", "jinzhu").First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;

// Not In
db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");

// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
//// SELECT * FROM users WHERE id NOT IN (1,2,3);

db.Not([]int64{}).First(&user)
//// SELECT * FROM users;

// Plain SQL
db.Not("name = ?", "jinzhu").First(&user)
//// SELECT * FROM users WHERE NOT(name = "jinzhu");

// Struct
db.Not(User{Name: "jinzhu"}).First(&user)
//// SELECT * FROM users WHERE name <> "jinzhu";

带内联条件的查询

注意:使用主键查询时,应仔细检查所传递的值是否为有效主键,以避免SQL注入

// 按主键获取
db.First(&user, 23)
//// SELECT * FROM users WHERE id = 23 LIMIT 1;

// 简单SQL
db.Find(&user, "name = ?", "jinzhu")
//// SELECT * FROM users WHERE name = "jinzhu";

db.Find(&users, "name <> ? AND age > ?", "jinzhu", 20)
//// SELECT * FROM users WHERE name <> "jinzhu" AND age > 20;

// Struct
db.Find(&users, User{Age: 20})
//// SELECT * FROM users WHERE age = 20;

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

Or条件查询

db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
//// SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';

// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';

// Map
db.Where("name = 'jinzhu'").Or(map[string]interface{}{"name": "jinzhu 2"}).Find(&users)

查询链

Gorm有一个可链接的API,你可以这样使用它

db.Where("name <> ?","jinzhu").Where("age >= ? and role <> ?",20,"admin").Find(&users)
//// SELECT * FROM users WHERE name <> 'jinzhu' AND age >= 20 AND role <> 'admin';

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

扩展查询选项

// 为Select语句添加扩展SQL选项
db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10)
//// SELECT * FROM users WHERE id = 10 FOR UPDATE;

FirstOrInit

获取第一个匹配的记录,或者使用给定的条件初始化一个新的记录(仅适用于struct,map条件)

// Unfound
db.FirstOrInit(&user, User{Name: "non_existing"})
//// user -> User{Name: "non_existing"}

// Found
db.Where(User{Name: "Jinzhu"}).FirstOrInit(&user)
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}
db.FirstOrInit(&user, map[string]interface{}{"name": "jinzhu"})
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}

Attrs

如果未找到记录,则使用参数初始化结构

// Unfound
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = 'non_existing';
//// user -> User{Name: "non_existing", Age: 20}

db.Where(User{Name: "non_existing"}).Attrs("age", 20).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = 'non_existing';
//// user -> User{Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "Jinzhu"}).Attrs(User{Age: 30}).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = jinzhu';
//// user -> User{Id: 111, Name: "Jinzhu", Age: 20}

Assign

将参数分配给结果,不管它是否被找到

db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrInit(&user)
//// user -> User{Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "Jinzhu"}).Assign(User{Age: 30}).FirstOrInit(&user)
//// SELECT * FROM USERS WHERE name = jinzhu';
//// user -> User{Id: 111, Name: "Jinzhu", Age: 30}

FirstOrCreate

获取第一个匹配的记录,或创建一个具有给定条件的新记录(仅适用于struct, map条件)

// Unfound
db.FirstOrCreate(&user, User{Name: "non_existing"})
//// INSERT INTO "users" (name) VALUES ("non_existing");
//// user -> User{Id: 112, Name: "non_existing"}

// Found
db.Where(User{Name: "Jinzhu"}).FirstOrCreate(&user)
//// user -> User{Id: 111, Name: "Jinzhu"}

Attrs

如果未找到记录,则为参数分配结构

    // Unfound
    db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
    //// SELECT * FROM users WHERE name = 'non_existing';
    //// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
    //// user -> User{Id: 112, Name: "non_existing", Age: 20}

    // Found
    db.Where(User{Name: "jinzhu"}).Attrs(User{Age: 30}).FirstOrCreate(&user)
    //// SELECT * FROM users WHERE name = 'jinzhu';
    //// user -> User{Id: 111, Name: "jinzhu", Age: 20}

Assign

将其分配给记录,而不管它是否被找到,并保存回数据库。

// Unfound
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'non_existing';
//// INSERT INTO "users" (name, age) VALUES ("non_existing", 20);
//// user -> User{Id: 112, Name: "non_existing", Age: 20}

// Found
db.Where(User{Name: "jinzhu"}).Assign(User{Age: 30}).FirstOrCreate(&user)
//// SELECT * FROM users WHERE name = 'jinzhu';
//// UPDATE users SET age=30 WHERE id = 111;
//// user -> User{Id: 111, Name: "jinzhu", Age: 30}

Select

指定要从数据库检索的字段,默认情况下,将选择所有字段;

db.Select("name, age").Find(&users)
//// SELECT name, age FROM users;

db.Select([]string{"name", "age"}).Find(&users)
//// SELECT name, age FROM users;

db.Table("users").Select("COALESCE(age,?)", 42).Rows()
//// SELECT COALESCE(age,'42') FROM users;

Order

在从数据库检索记录时指定顺序,将重排序设置为true以覆盖定义的条件

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;

// ReOrder
db.Order("age desc").Find(&users1).Order("age", true).Find(&users2)
//// SELECT * FROM users ORDER BY age desc; (users1)
//// SELECT * FROM users ORDER BY age; (users2)

Limit

指定要检索的记录数

db.Limit(3).Find(&users)
//// SELECT * FROM users LIMIT 3;

// Cancel limit condition with -1
db.Limit(10).Find(&users1).Limit(-1).Find(&users2)
//// SELECT * FROM users LIMIT 10; (users1)
//// SELECT * FROM users; (users2)

Offset

指定在开始返回记录之前要跳过的记录数

db.Offset(3).Find(&users)
//// SELECT * FROM users OFFSET 3;

// Cancel offset condition with -1
db.Offset(10).Find(&users1).Offset(-1).Find(&users2)
//// SELECT * FROM users OFFSET 10; (users1)
//// SELECT * FROM users; (users2)

Count

获取模型的记录数

    db.Where("name = ?", "jinzhu").Or("name = ?", "jinzhu 2").Find(&users).Count(&count)
    //// SELECT * from USERS WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (users)
    //// SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2'; (count)

    db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
    //// SELECT count(*) FROM users WHERE name = 'jinzhu'; (count)

    db.Table("deleted_users").Count(&count)
    //// SELECT count(*) FROM deleted_users;

Group & Having

HAVING语句通常与[GROUP BY](zhidao.baidu.com/search?word… BY&fr=iknow_pc_qb_highlight)语句联合使用,用来过滤由GROUP BY语句返回的记录集。

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Rows()
for rows.Next() {
    ...
}

rows, err := db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Rows()
for rows.Next() {
    ...
}

type Result struct {
    Date  time.Time
    Total int64
}
db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)

Join

指定连接条件

rows, err := db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Rows()
for rows.Next() {
    ...
}

db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)

// 多个连接与参数
db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)

Pluck

将模型中的单个列作为地图查询,如果要查询多个列,可以使用Scan

var ages []int64
db.Find(&users).Pluck("age", &ages)

var names []string
db.Model(&User{}).Pluck("name", &names)

db.Table("deleted_users").Pluck("name", &names)

// 要返回多个列,做这样:
db.Select("name, age").Find(&users)

Scan

将结果扫描到另一个结构中。

type Result struct {
    Name string
    Age  int
}

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

// Raw SQL
db.Raw("SELECT name, age FROM users WHERE name = ?", 3).Scan(&result)

Scopes

将当前数据库连接传递到func(*DB) *DB,可以用于动态添加条件

func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
    return db.Where("amount > ?", 1000)
}

func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func PaidWithCod(db *gorm.DB) *gorm.DB {
    return db.Where("pay_mode_sign = ?", "C")
}

func OrderStatus(status []string) func (db *gorm.DB) *gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
        return db.Scopes(AmountGreaterThan1000).Where("status in (?)", status)
    }
}

db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
// 查找所有信用卡订单和金额大于1000

db.Scopes(AmountGreaterThan1000, PaidWithCod).Find(&orders)
// 查找所有COD订单和金额大于1000

db.Scopes(OrderStatus([]string{"paid", "shipped"})).Find(&orders)
// 查找所有付费,发货订单

指定表名

// 使用User结构定义创建`deleted_users`表
db.Table("deleted_users").CreateTable(&User{})

var deleted_users []User
db.Table("deleted_users").Find(&deleted_users)
//// SELECT * FROM deleted_users;

db.Table("deleted_users").Where("name = ?", "jinzhu").Delete()
//// DELETE FROM deleted_users WHERE name = 'jinzhu';

GORM 如何读取别名字段的值

表结构

DROP TABLE IF EXISTS "test"."test";
CREATE TABLE "test"."test" ("id" varchar(32) NOT NULL,"name" varchar(255) COLLATE "pg_catalog"."default","remark" varchar(255) COLLATE "pg_catalog"."default"
);
ALTER TABLE "test"."test" ADD CONSTRAINT "test_pkey" PRIMARY KEY ("id");

表对应模型结构体

type Test struct {ID     string  `gorm:"column:id;type:varchar(32);primaryKey;comment:唯一 ID,流水号" json:"id"` // 唯一 ID,流水号
    Name   string `gorm:"column:name;type:varchar(255);comment:名称" json:"name"` // 名称
    Remark string  `gorm:"column:remark;type:varchar(255);comment:备注" json:"remark"` // 备注
 
    MoreInfo string `gorm:"-" json:"moreInfo"` // 更多信息,非表结构字段
}

GORM 查询

gormDB 为已初始化的 gorm 数据库连接。

test := Test{ID: "0000000001"}
gormDB.Select("*, 'testMoreInfoVal' AS more_Info").Where(&test).Find(&test)

软删除

如果模型(结构体)包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得 软删除 的能力。 比如要在 product 中开启软删除,可以这样定义模型:

type Product struct {
    ID uint
    Code string `gorm:"column:code"`
    Price uint `gorm:"column:price"`
    Deleted gorm.DeletedAt
}

或者直接引入 gorm.Model)

拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且不能再通过普通的查询方法找到该记录。

软删除的原理就是 GORM 在查询时会在 SQL 语句后拼接一个 where 条件来检查 deleted_at 字段,如果为空说明没有删除,不为空说明这条数据应该被删除,查询出来的结果就把它过滤了。

可以使用 Unscoped 方法找到被软删除的记录或是永久删除匹配的记录:

db.Unscoped().Where("code = U89").Find(&products)
db.Unscoped().Delete(&p)

总结

整理了常见gorm的操作。

参考

关于 GORM 看这一篇就够了 | 青训营笔记 juejin.cn/post/719553…