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 mainimport ( "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 mainimport ( "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 name1 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篇诗文 62 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篇诗文 83 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 poetimport "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 modelimport "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) }
效果演示:
image.png
image.png
image.png
实际操作的SQL 语句:
image.png
作者:谢小路
链接:https://www.jianshu.com/p/bc5aa5107458