『No19: Gorm 上手指南』 - Go语言中文社区

『No19: Gorm 上手指南』


image.png

大家好,我叫谢伟,是一名程序员。

如果你是做后端开发的,日常工作中,除了熟悉编程语言之外,数据库怕是最常用的技术了吧。

比如搭建一个Web后台管理系统,你需要数据吧,你总不能指望网页都是静态数据吧。需要数据,那么就要和数据库打交道。日常开发中你可能会使用关系型数据库,比如 MySQL、PostgresSQL,也可能使用NoSQL型数据库,比如MongoDB,redis等,甚至会使用各种各样的符合特定场景下的数据库。

但我建议,至少需要熟练掌握一门关系型数据库,日常开发中你会发现绝大多数的需求的实现都需要和数据库打交道。你仅仅只会简单的增删改查,是不太够用的。仅仅只会在编程语言层面编写简单SQL,也是不太够用。

你需要会:

  • 数据库的设计:数据库设计三大范式
  • 数据库多表操作
  • 数据库服务端操作
  • 备份恢复
  • 事务等操作
  • 分库分表等操作

本节的主题:gorm 的使用。

大纲:

  • 原生database/sql 接口
  • 丰富的第三方驱动
  • gorm 的使用

1. 原生 database/sql 接口

Go 官方并没有提供数据库驱动,只定义了一些标准的接口。所以你会看看各种各样的第三方驱动。

本质上都在实现官方提供的标准接口。

sql.Register

作用用来注册数据库驱动。

所以你使用第三方数据库驱动,最重要的一点就是要导入该库,实现第三方数据库驱动的init 函数。该init 函数即是实现注册数据库驱动。

sqlite3 导入: _ "github.com/mattn/go-sqlite3"

init 函数


func init() {
    sql.Register("sqlite3", &SQLiteDriver{})
}

原生函数如下:

func Register(name string, driver driver.Driver) {
    driversMu.Lock()
    defer driversMu.Unlock()
    if driver == nil {
        panic("sql: Register driver is nil")
    }
    if _, dup := drivers[name]; dup {
        panic("sql: Register called twice for driver " + name)
    }
    drivers[name] = driver
}

可以看出,核心是driver.Driver接口,定义了一个 open 方法

type Driver interface {
    // Open returns a new connection to the database.
    // The name is a string in a driver-specific format.
    //
    // Open may return a cached connection (one previously
    // closed), but doing so is unnecessary; the sql package
    // maintains a pool of idle connections for efficient re-use.
    //
    // The returned connection is only used by one goroutine at a
    // time.
    Open(name string) (Conn, error)
}

Conn 是一个数据库连接接口,定义了Prepare、Close、Begin方法

  • Prepare:SQL语句准备阶段
  • Close: 关闭连接
  • Begin: 事务处理
type Conn interface {
    // Prepare returns a prepared statement, bound to this connection.
    Prepare(query string) (Stmt, error)

    // Close invalidates and potentially stops any current
    // prepared statements and transactions, marking this
    // connection as no longer in use.
    //
    // Because the sql package maintains a free pool of
    // connections and only calls Close when there's a surplus of
    // idle connections, it shouldn't be necessary for drivers to
    // do their own connection caching.
    Close() error

    // Begin starts and returns a new transaction.
    //
    // Deprecated: Drivers should implement ConnBeginTx instead (or additionally).
    Begin() (Tx, error)
}

Stmt是一种准备好的状态,和Conn相关联,而且只能应用于一个goroutine中,不能应用于多个goroutine。


type Stmt interface {
    // Close closes the statement.
    //
    // As of Go 1.1, a Stmt will not be closed if it's in use
    // by any queries.
    Close() error

    // NumInput returns the number of placeholder parameters.
    //
    // If NumInput returns >= 0, the sql package will sanity check
    // argument counts from callers and return errors to the caller
    // before the statement's Exec or Query methods are called.
    //
    // NumInput may also return -1, if the driver doesn't know
    // its number of placeholders. In that case, the sql package
    // will not sanity check Exec or Query argument counts.
    NumInput() int

    // Exec executes a query that doesn't return rows, such
    // as an INSERT or UPDATE.
    //
    // Deprecated: Drivers should implement StmtExecContext instead (or additionally).
    Exec(args []Value) (Result, error)

    // Query executes a query that may return rows, such as a
    // SELECT.
    //
    // Deprecated: Drivers should implement StmtQueryContext instead (or additionally).
    Query(args []Value) (Rows, error)
}

大概就是定义了一系列的标准接口,接口内注意输入参数和返回值,然后一步步下去,就大概可以知道官方的接口是如何定义的。

官方的这些接口,需要被第三方数据库驱动实现,不管是sqlite、mysql、PostgresSQL 都需要实现这些接口,实际的使用过程中调用这些接口即可。

比如:

数据库表:

id created_at updated_at deleted_at type_name
1 "2018-08-11 13:29:45.773658+08" "2018-08-11 13:29:45.773658+08" "诗"
2 "2018-08-11 13:29:45.779012+08" "2018-08-11 13:29:45.779012+08" "词"
3 "2018-08-11 13:29:45.781381+08" "2018-08-11 13:29:45.781381+08" "曲"
4 "2018-08-11 13:29:45.784009+08" "2018-08-11 13:29:45.784009+08" "文言文"

如何获取这4条记录:


package main

import (
    "database/sql"

    "fmt"

    "time"

    _ "github.com/jinzhu/gorm/dialects/postgres"
)

func main() {
    db, _ := sql.Open("postgres", "host=127.0.0.1 user=xiewei dbname=crawler_info port=5432 sslmode=disable password=admin")

    rows, _ := db.Query(`select * from poetry_types limit 4`)
    fmt.Println(rows.Columns())
    for rows.Next() {
        var id int
        var createdAt time.Time
        var updatedAt time.Time
        var deletedAt *time.Time
        var typeName string
        rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &typeName)
        fmt.Println(id, createdAt, updatedAt, deletedAt, typeName)
    }

}

使用这种方法,你只需知道原生database/sql 的接口是如何调用的即可,具体的实现由第三方已实现。

知道数据库(crawler_info), 知道数据库表(poetry_type),知道数据库内字段的类型( id int, createdAt time.Time, updatedAt time.Time, deletedAt *time.Time, typeName string)

即可写SQL 语句操作数据库,实现增删改查。

这种接口的定义有什么好处?

不管你操作sqlite、mysql、PostgreSQL 等数据库,只是连接驱动这块不一致,其他的操作一模一样。

# postgresql
    db, _ := sql.Open("postgres", "host=127.0.0.1 user=xiewei dbname=crawler_info port=5432 sslmode=disable password=admin")

# sqlite
    db, _ := sql.Open("sqlite3", "*****")

# mysql
    db, _ := sql.Open("mysql", "*****")


2. gorm

使用上述方法的缺点是使你代码内充斥着 SQL 语句。使用 ORM (对象关系映射)可以解决这个问题,使我们操作对象即可达到操作数据库的目的。

gorm 的使用步骤:

  • 定义model 即对象层(知道操作的对象是谁)
  • 建立连接
  • 创建数据表(数据库中存在表也可不执行该步,定义model 即可,字段变更会新增字段)
  • 操作数据库

package main

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


// 声明数据库表的形式

type Product struct {
  gorm.Model
  Code string
  Price uint
}

func main() {

  // 建立连接
  db, err := gorm.Open("sqlite3", "test.db")
  if err != nil {
    panic("failed to connect database")
  }
  defer db.Close()

  // 生成数据库表
  db.AutoMigrate(&Product{})

  // 新增一条记录:将Product 对象转换成数据库内一条记录
  db.Create(&Product{Code: "L1212", Price: 1000})

  // 获取对象:将数据库内一条记录转换成 product 对象
  var product Product
  db.First(&product, 1) // find product with id 1
  db.First(&product, "code = ?", "L1212") // find product with code l1212

  // 更新记录
  db.Model(&product).Update("Price", 2000)

  // 删除记录
  db.Delete(&product)
}

使用的orm 技术的关键在于理解,对象和数据库表的映射。

1. 要操作数据库内已存在的数据表怎么使用 orm ?

  • 定义和数据库内对应的数据表的model

数据内存在这么一张表(dynasties):

id  created_at                  updated_at              deleted_at  name
1   2018-08-11 05:29:45.647432  2018-08-11 05:29:45.647432      先秦
2   2018-08-11 05:29:45.660827  2018-08-11 05:29:45.660827      两汉
3   2018-08-11 05:29:45.664372  2018-08-11 05:29:45.664372      魏晋
4   2018-08-11 05:29:45.668520  2018-08-11 05:29:45.668520      南北朝
5   2018-08-11 05:29:45.672059  2018-08-11 05:29:45.672059      隋代
6   2018-08-11 05:29:45.675465  2018-08-11 05:29:45.675465      唐代
7   2018-08-11 05:29:45.678486  2018-08-11 05:29:45.678486      五代
8   2018-08-11 05:29:45.680459  2018-08-11 05:29:45.680459      宋代
9   2018-08-11 05:29:45.682840  2018-08-11 05:29:45.682840      金朝
10  2018-08-11 05:29:45.684378  2018-08-11 05:29:45.684378      元代
11  2018-08-11 05:29:45.686548  2018-08-11 05:29:45.686548      明代
12  2018-08-11 05:29:45.688638  2018-08-11 05:29:45.688638      清代
13  2018-08-11 12:02:59.433187  2018-08-11 12:02:59.433187      近代
14  2018-08-11 12:29:02.976485  2018-08-11 12:29:02.976485      现代
15  2018-08-11 12:29:52.015595  2018-08-11 12:29:52.015595      未知

则定义 model 对象 :

type Dynasty struct {
    gorm.Model
    Name string `gorm:"tye:varchar" json:"name"`
}

gorm.model gorm 内置的 格式如下:

type Model struct {
    ID        uint `gorm:"primary_key"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time `sql:"index"`
}
  • 默认取定义的结构体的小写的复数形式为数据库表名(Dynasty/dynasties)

建立连接即可操作:

db, err := gorm.open("postgres", "*****")
  • 获取记录:

newDynastyRecord 即获取到id = 1 的一条记录。该条记录的值填充在newDynastyRecord内。

比如获取该条记录的name: 即 newDynastyRecord.Name


var newDynastyRecord Dynasty

db.Where("id = ?", 1).First(&newDynastyRecord)

fmt.Println(newDynastyRecord.Name)

var newDynastyRecordList []Dynasty

db.Find(&newDynastyRecordList)

fmt.Println(len(newDynastyRecordList)) // 15

  • Where 限定条件,和 sql 语句中的 Where 用法差不多
  • First 即获取满足条件的第一条记录
  • Find 可以获取满足条件的所有记录

2. 数据库内不存在数据库表怎么操作

  • 定义 model 对象
type PoetryType struct {
    gorm.Model
    TypeName string `gorm:"type:varchar" json:"type_name"`
}

不存在的表,首先需要创建表:db.AutoMigrate(&PoetryType)

创建之后操作:

即把新增的记录新增入poetry_types 表内

var poetryType PoetryType
poetryType = PoetryType{
    Typaname: "诗"
}

db.create(&poetryType)

核心只有一条,在 gorm 内操作对象(struct),以达到操作数据表的目的。有对象,没数据库表,操作失败;没对象,有数据库表,操作失败。找不到对应关系,操作失败; 对应关系搞错,操作失败。

3. 实例

我这边利用爬虫技术,把一系列的诗人的基本信息,诗人的诗文,朝代和诗的类型入库了。

大概的样子如下:

dynasties 表:朝代

1   2018-08-11 05:29:45.647432  2018-08-11 05:29:45.647432      先秦
2   2018-08-11 05:29:45.660827  2018-08-11 05:29:45.660827      两汉
3   2018-08-11 05:29:45.664372  2018-08-11 05:29:45.664372      魏晋

poetry_types 表:诗类型


1   2018-08-11 05:29:45.773658  2018-08-11 05:29:45.773658      诗
2   2018-08-11 05:29:45.779012  2018-08-11 05:29:45.779012      词
3   2018-08-11 05:29:45.781381  2018-08-11 05:29:45.781381      曲

poetry_infos 表:诗文的具体内容

1   2018-08-11 13:39:37.341955  2018-08-11 13:39:37.341955      将进酒 君不见,黄河之水天上来,奔流到海不复回。君不见,高堂明镜悲白发,朝如青丝暮成雪。人生得意须尽欢,莫使金樽空对月。天生我材必有用,千金散尽还复来。烹羊宰牛且为乐,会须一饮三百杯。岑夫子,丹丘生,将进酒,杯莫停。与君歌一曲,请君为我倾耳听。(倾耳听 一作:侧耳听)钟鼓馔玉不足贵,但愿长醉不复醒。(不足贵 一作:何足贵;不复醒 一作:不愿醒/不用醒)古来圣贤皆寂寞,惟有饮者留其名。(古来 一作:自古;惟 通:唯)陈王昔时宴平乐,斗酒十千恣欢谑。主人何为言少钱,径须沽取对君酌。五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。   34290   1   6
2   2018-08-11 13:39:37.414925  2018-08-11 13:39:37.414925      水调歌头·明月几时有  "丙辰中秋,欢饮达旦,大醉,作此篇,兼怀子由。
明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间?(何似 一作:何时;又恐 一作:惟 / 唯恐)转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。(长向 一作:偏向)"    33531   2   8

poets 表:诗人信息

1   2018-08-11 13:39:37.334029  2018-08-11 13:39:37.334029      李白  https://img.gushiwen.org/authorImg/libai.jpg    1310    6169    李白(701年-762年),字太白,号青莲居士,唐朝浪漫主义诗人,被后人誉为“诗仙”。祖籍陇西成纪(待考),出生于西域碎叶城,4岁再随父迁至剑南道绵州。李白存世诗文千余篇,有《李太白集》传世。762年病逝,享年61岁。其墓在今安徽当涂,四川江油、湖北安陆有纪念馆。 1310篇诗文    6
2   2018-08-11 13:39:37.412562  2018-08-11 13:39:37.412562      苏轼  https://img.gushiwen.org/authorImg/sushi.jpg    3637    4024    苏轼(1037-1101),北宋文学家、书画家、美食家。字子瞻,号东坡居士。汉族,四川人,葬于颍昌(今河南省平顶山市郏县)。一生仕途坎坷,学识渊博,天资极高,诗文书画皆精。其文汪洋恣肆,明白畅达,与欧阳修并称欧苏,为“唐宋八大家”之一;诗清新豪健,善用夸张、比喻,艺术表现独具风格,与黄庭坚并称苏黄;词开豪放一派,对后世有巨大影响,与辛弃疾并称苏辛;书法擅长行书、楷书,能自创新意,用笔丰腴跌宕,有天真烂漫之趣,与黄庭坚、米芾、蔡襄并称宋四家;画学文同,论画主张神似,提倡“士人画”。著有《苏东坡全集》和《东坡乐府》等。 3637篇诗文    8
3   2018-08-11 13:39:37.495146  2018-08-11 13:39:37.495146      陶渊明 https://img.gushiwen.org/authorImg/taoyuanming.jpg  186 1466    陶渊明(约365年—427年),字元亮,(又一说名潜,字渊明)号五柳先生,私谥“靖节”,东晋末期南朝宋初期诗人、文学家、辞赋家、散文家。汉族,东晋浔阳柴桑人(今江西九江)。曾做过几年小官,后辞官回家,从此隐居,田园生活是陶渊明诗的主要题材,相关作品有《饮酒》、《归园田居》、《桃花源记》、《五柳先生传》、《归去来兮辞》等。 186篇诗文    3

基于上述的信息,构建如下API 服务:

图片发自简书App

核心步骤如下:

  • 建立 gin api server
  • 根据上文的数据库表定义对象 model
  • gin 路由和控制器的编写

api server

func GinInit() {
    r := gin.New()
    gin.SetMode(gin.DebugMode)

    r.Use(gin.Logger())
    //r.Use(gin.Recovery())

    v1 := r.Group("/v1/api")
    {
        poet.Register(v1) // 所有的路由和控制器
    }
    getAllApi(r)

    r.Run(":8080")

}

路由和控制器

package poet

import "github.com/gin-gonic/gin"

func Register(r *gin.RouterGroup) {
    r.GET("/poet/:id", ShowPoetHandler)
    r.GET("/poetry/:id", ShowPoetryHandler)
    r.GET("/dynasty/:id", ShowDynastyHandler)
    r.GET("/poetryType/:id", ShowPoetTypeHandler)
    r.GET("/gushiwen/poet", ShowListPoetHandler)
    r.GET("/gushiwen/poetry", ShowListPoetryHandler)
    r.GET("/gushiwen/dynasty", ShowListDynastyHandler)
    r.GET("/gushiwen/poetryType", ShowListPoetTypeHandler)
}


model

package model

import "github.com/jinzhu/gorm"

// 朝代的表: 先秦、两汉、魏晋、南北朝、隋代、唐代、五代、宋代、金朝、元代、明代、清代...
type Dynasty struct {
    gorm.Model
    Name string `gorm:"tye:varchar" json:"name"`
}

// 诗人的表
type Poet struct {
    gorm.Model
    Name        string `gorm:"type:varchar" json:"name"`
    ImageURL    string `gorm:"type:varchar" json:"image_url"`
    Number      uint   `gorm:"type:integer" json:"number"`
    Liked       uint   `gorm:"type:integer" json:"liked"`
    Description string `gorm:"type:varchar" json:"description"`
    DynastyID   uint   `gorm:"type:integer" json:"dynasty_id"`
    PoetryInfo  []PoetryInfo
}

// 诗类型的表: 诗、词、曲、文言文
type PoetryType struct {
    gorm.Model
    TypeName string `gorm:"type:varchar" json:"type_name"`
}

// 诗文的表
type PoetryInfo struct {
    gorm.Model
    Title     string `gorm:"type:varchar" json:"title"`
    Content   string `gorm:"type:varchar" json:"content"`
    Liked     uint   `gorm:"type:integer;default(0)" json:"liked"`
    PoetID    uint   `gorm:"type:integer" json:"poet_id"`
    DynastyID uint   `gorm:"type:integer" json:"dynasty_id"`
}

路由 1 和 控制器 1

r.GET("/poet/:id", ShowPoetHandler)

func NotFound() (int, map[string]interface{}) {
    return http.StatusBadGateway, gin.H{"Message": "not found record"}

}

func ShowPoetHandler(c *gin.Context) {
    id, _ := strconv.Atoi(c.Param("id"))

    var poet model.Poet
    if dbError := initial.DataBase.Where("id = ?", id).First(&poet).Error; dbError != nil {
        c.JSON(NotFound())
        return
    }
    c.JSON(http.StatusOK, poet)

}

路由 2 和控制 2

r.GET("/gushiwen/poet", ShowListPoetHandler)

type ListPoetParams struct {
    CommonPagination
    Search     string `form:"search" json:"search"`
    Return     string `form:"return" json:"return" binding:"omitempty,eq=all_list|eq=all_count"`
    PoetNumber uint   `form:"number" json:"number"`
    Like       uint   `form:"like" json:"like"`
    Dynasty    string `form:"dynasty" json:"dynasty"`
}

func ShowListPoetHandler(c *gin.Context) {

    var param ListPoetParams
    if err := c.ShouldBind(&param); err != nil {
        fmt.Println("error", err)
        return
    }
    offset := param.PerPage * (param.Page - 1)
    order := param.OrderBy + " " + param.SortBy

    var poet []model.Poet
    query := initial.DataBase.Preload("PoetryInfo").Model(&poet)
    if param.Return != "all_list" {
        query = query.Offset(offset).Limit(param.PerPage)
    }

    if param.Search != "" {
        query = query.Where("name = ?", param.Search)
    }

    if param.PoetNumber != 0 {
        query = query.Where("number < ?", param.PoetNumber)
    }

    if param.Like != 0 {
        query = query.Where("liked < ?", param.Like)
    }

    if param.Dynasty != "" {
        var dynasty model.Dynasty
        if dbError := initial.DataBase.Where("name = ?", param.Dynasty).First(&dynasty).Error; dbError != nil {
            c.JSON(NotFound())
            return
        }
        query = query.Where("dynasty_id = ?", dynasty.ID)
    }
    query.Order(order).Find(&poet)
    c.JSON(http.StatusOK, poet)
}

效果演示:

image.png
image.png
image.png

实际操作的SQL 语句:

image.png

参考代码: 参考代码

  1. 使用的PostgreSQL 数据库,确保本机有服务启动
  2. 先db 下的压缩包使用 pg_restore 命令导入数据库
  3. 修改 configs/setting.yml 数据库配置
  4. make install 安装依赖库
  5. make dev 启动服务

全文完,再会。

多看文档

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/bc5aa5107458
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-12 12:06:38
  • 阅读 ( 971 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢