大家好,我叫谢伟,是一名程序员。
如果你是做后端开发的,日常工作中,除了熟悉编程语言之外,数据库怕是最常用的技术了吧。
比如搭建一个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 服务:
核心步骤如下:
- 建立 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(¶m); 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)
}
效果演示:
实际操作的SQL 语句:
参考代码: 参考代码
- 使用的PostgreSQL 数据库,确保本机有服务启动
- 先db 下的压缩包使用 pg_restore 命令导入数据库
- 修改 configs/setting.yml 数据库配置
- make install 安装依赖库
- make dev 启动服务
全文完,再会。
多看文档