Golang ORM框架 — GORM

Golang ORM框架 — GORM

互联网开发最重要的一部分就是与数据库的交互,该部分在我们分层互联网模型中会归属于 — 模型层 + 仓库层。ORM — Object Relational Mapping,即代码模型与数据库模型(主要指向关系型数据库模型)之间的映射。熟悉 Java 网络编程的同学可能都接触过 MyBatis、Hibernate等ORM框架,这些框架大大地减少了我们与数据库之间交互的繁杂性。

Golang 作为21世纪新兴的编程语言,出生就自带了数据库交互组件,gosdk 中 database/sql 包就是对数据库提供支持的组件包。

但是,在本次文档中我们就先跳过原生的数据库组件,先从进阶的 gorm 包开始讨论。(PS:不讨论实现原理)

在接下来的文档里,我将以一种简单的互联网项目的思路去设计模型层 + 仓库层:

数据库配置模型

建立目录 ${ROOT}/model/conf,在内部创建文件 db.go。编写一下结构体:

type DB struct {
	Dialect   string `yaml:"dialect"`		// 数据库语言
	Username  string `yaml:"username"`		// 数据库连接用户名
	Password  string `yaml:"password"`		// 数据库连接密码
	Name      string `yaml:"name"`			// 数据库名
	Host      string `yaml:"host"`			// 数据库连接服务器地址
	Port      int    `yaml:"port"`			// 数据库连接服务器端口号
	Query     string `yaml:"query"`			// 数据库连接使用的extra参数
	DebugMode bool   `yaml:"debugMode"`		// 数据库是否进入debug模式
}

创建上述结构体的原因在于,我们在项目中将要使用yaml文件的方式去进行配置,对go-yaml不是很熟悉的同学可以阅读我博客中关于go解析yaml的文档:Golang — 解析yaml

使用上述结构体中的部分参数我们可以组成我们连接数据库使用的dsn:

func (d *DB) DSN() string {
	return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", d.Username, d.Password, d.Host, d.Port, d.Name, d.Query)
}

“打开” 数据库

创建文件 ${ROOT}/dao/init.go,这是我们初始化数据库的文件。这里我们定义两个非出口的全局变量:

var (
	db    *gorm.DB	// 数据库指针
	debug bool			// 是否是debug模式
)

这里教大家一种互联网公司经常使用的初始化函数模型 InitMustInit。这两个单词字面意思就是初始化和必须初始化,其实在实践中也是同样的意思,初始化时发生错误我就返回错,必须初始化时发生错误我就 panic

// Init 初始化数据库,发生错误时返回错误
func Init(config *conf.DB) error {
	debug = config.DebugMode
	var err error
	db, err = gorm.Open(config.Dialect, config.DSN())
	return err
}

// MustInit 初始化数据库,发生错误时 panic
func MustInit(config *conf.DB) {
	if err := Init(config); err != nil {
		panic(err)
	}
}

这里我们第一次接触到了 gorm 包中的函数 Opengorm.Open 接受 DialectDSN 作为参数,初始化 gorm.DB 对象并返回指向其的指针。这个过程中,我们要将指针存到我们之前定义的全局变量 db 中。

有同学肯定已经注意到了,我们在 model/conf/db.go 中定义的配置结构体在这我们就很方便地使用到了。

由于我们定义的全局变量 db 是非出口的(小写字母开头),我们就需要定义一个 getter。在这个 getter 中我们要对 db 对象做一些前置的处理:

// DB 使用全局变量 debug 和参数 tableName 对全局变量 db 做前置处理后返回
func DB(tableName string) *gorm.DB {
	if debug {
		return db.Debug().Table(tableName)
	}
	return db.Table(tableName)
}

db.Debug() 可以使 db 进入debug模式,开始打印出所有执行的sql语句。db.Table 可以指定db接下来的操作要在哪一个table上进行。

gorm.DB 的链式模型

如果你有心去观察 db.Opendb.Debug()db.Table 等函数你会发现,这些函数都有一个共同点:他们的 reveiver都是 gorm.DB 的指针,返回值也是 gorm.DB 的指针。实际上,在db上面的操作都会生成一个新的db对象,我们来看看 db.Debug 的源码:

// Debug start debug mode
func (s *DB) Debug() *DB {
	return s.clone().LogMode(true)
}

s.clone().LogMode(true) 就是克隆了一个新的对象,将其设为debug模型并返回。这就是为什么我们要将全局变量 db 设为非出口的,db 的属性不能被任何人改动,任何其他人获取到的都是 db 的克隆。

模型定义

定义模型之前先为大家介绍 gorm 中预置的模型结构体 gorm.Model

// Model base model definition, including fields `ID`, `CreatedAt`, `UpdatedAt`, `DeletedAt`, which could be embedded in your models
//    type User struct {
//      gorm.Model
//    }
type Model struct {
	ID        uint `gorm:"primary_key"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt *time.Time `sql:"index"`
}

该结构体中包含了各种数据库模型共同具有的一些参数,包括主键、创建时间、更新时间和删除时间。我们以 go 语言的内嵌结构体的形式将其插入到我们的结构体当中。这样 gorm 就会感知到这些参数的存在,并在一般的数据库操作中为我们自动更新这些参数(PS:在进行Query时,gorm 会去判断 DeletedAt 是否为 nil,只返回那些 DeletedAt 不为 nil 的记录)。

接下来,我们就开始定义我们的数据库模型:

作者-书籍模型

我们就以简单的作者-书籍模型为例:

先定义作者模型 Author。创建文件 ${ROOT}/model/author/author.go

package author

import (
	"github.com/elzatahmed/go-gorm/dao"
	"github.com/jinzhu/gorm"
)

// 以非出口的形式创建gender类型
// 因为gender类型只能是我们自定义的,不能由用户自定义
type gender int8

// 只定义两种性别,不能自定义
const (
	GenderMale gender = iota + 1
	GenderFemale
)

// Author 为作者模型
type Author struct {
  // 嵌入gorm.Model
	gorm.Model
	Name   string `gorm:"column:name"`			// 名字
	Gender gender `gorm:"column:gender"`		// 性别
	Age    int    `gorm:"age"`							// 岁数
}

// New 创建新的 Author 对象并返回其指针
func New(name string, gender gender, age int) *Author {
	return &Author{
		Name:   name,
		Gender: gender,
		Age:    age,
	}
}

// TableName是在使用gorm时不传递表名的情况下,被gorm调用获取表名的方法
func (a Author) TableName() string {
	return dao.TableNameAuthor
}

其次以同样的逻辑定义书籍模型 Book,创建文件 ${ROOT}/model/book/book.go

package book

import (
	"github.com/elzatahmed/go-gorm/dao"
	"github.com/jinzhu/gorm"
)

type Book struct {
  // 内嵌gorm.Model
	gorm.Model
	Title    string `gorm:"column:title"`					// 标题
	AuthorId uint   `gorm:"column:author_id"`			// 关联作者id
	Intro    string `gorm:"column:intro"`					// 简介
	Genre    string `gorm:"column:genre"`					// 体裁
}

// New 创建新的 Book 对象并返回其指针
func New(title, intro, genre string, authorId uint) *Book {
	return &Book{
		Title:    title,
		AuthorId: authorId,
		Intro:    intro,
		Genre:    genre,
	}
}

func (b Book) TableName() string {
	return dao.TableNameBook
}

在定义模型参数时我使用了 gorm 标签去定义一些额外参数,gorm 标签类型其实非常多,我们甚至可以完整的定义一列数据的所有属性并将DDL操作交给 gorm 去执行,以下是 gorm 典型的标签:

标签名描述
column列名
type类型
size数据大小/长度
primary_key主键标识
unique唯一键标识
not null不能为空
auto_increment自增记录

需要了解更多更详细的标签进入:gorm模型定义

CRUD

在一般的大型互联网项目中,模型和数据库操作是分隔开来的,数据库中每一个表都会有一个对应的 handler 处理各种不一样的 CRUD 请求。在本次项目中为了简单起见我将CUD(创建、更新和删除)操作直接编写到模型下。

在进入代码之间,我们先讨论讨论如何使用 gorm 的CRUD方法。

首先我们已经见到了 db.Table 方法,其作用是指定作用的数据库表。

其次最常用的还有 db.Where 方法:

// Where return a new relation, filter records with given conditions, accepts `map`, `struct` or `string` as conditions, refer http://jinzhu.github.io/gorm/crud.html#query
func (s *DB) Where(query interface{}, args ...interface{}) *DB {
	return s.clone().search.Where(query, args...).db
}

db.Where 的作用与 Sql 中的 Where 是一样的,即创建条件。其用法非常灵活,可以传入 string、map 甚至结构体。传入 string 即写入 sql query,将变量用 ?替代并将变量以 args 参数的形式传入:db.Where("id = ?", id)。而 map 和 struct 是以 key-value 的形式传入 query。

配合 db.Where 使用的方法有 db.Ordb.Not 等都是拼接query的方法。

在拼接完Query之后执行该Query的常用方法有两种:db.Firstdb.Finddb.First 返回数据库表中满足query的第一条记录(按照主键排序),db.Find 会返回满足条件的所有记录,根据不同 db.First 的输出一般会传入单个对象指针,db.Find 会传入slice。

(PS:其实 db.First db.Find 都支持直接在参数中传入where参数,但是在平常使用时我们会选利用 db.Where 构成链式的query)

db.Save 为创建记录的最常用的方法:

// Save update value in database, if the value doesn't have primary key, will insert it
func (s *DB) Save(value interface{}) *DB {
	scope := s.NewScope(value)
	if !scope.PrimaryKeyZero() {
		newDB := scope.callCallbacks(s.parent.callbacks.updates).db
		if newDB.Error == nil && newDB.RowsAffected == 0 {
			return s.New().Table(scope.TableName()).FirstOrCreate(value)
		}
		return newDB
	}
	return scope.callCallbacks(s.parent.callbacks.creates).db
}

注意要传入对象指针,Save会再将记录存入DB后将获取到的主键赋值到对应的主键域中。

记录的更新我们一般会利用 db.Updatedb.Updates,实际上这两个方法的使用方式类似,源码中 db.Update 调用 db.Updates 去实现功能。调用 db.Update 时我们可以传入多个参数,这意味着我们可以将列名与更新值分开传递(PS:这种方式只能更新一个),同时我们也可以利用 mapstruct 以键值对的形式去传递参数。

CUD:创建、更新和删除

进入 ${ROOT}/model/author/author.go 中,定义以下三个方法:

// 创建方法
func (a *Author) Save() error {
  // 获取DB、调用Save方法并传入对应值即可
	db := dao.DB(a.TableName()).Save(a)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

// 更新方法
func (a *Author) Update() error {
	fields := updateFields(a)
	db := dao.DB(a.TableName()).Where("id = ?", a.ID).Updates(fields)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

// 删除方法
func (a *Author) Delete() error {
	db := dao.DB(a.TableName()).Where("id = ?", a.ID).Delete(a)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

// 获取需要更新的域即不为零值的域
func updateFields(a *Author) (fields map[string]interface{}) {
	fields = make(map[string]interface{})
	if a.Name != "" {
		fields["name"] = a.Name
	}
	if a.Gender != 0 {
		fields["gender"] = a.Gender
	}
	if a.Age > 0 {
		fields["age"] = a.Age
	}
	return fields
}

进入 ${ROOT}/model/book/book.go 中,以同样的方式定义以下三个方法:

func (b *Book) Save() error {
	db := dao.DB(b.TableName()).Save(b)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

func (b *Book) Update() error {
	fields := updateFields(b)
	db := dao.DB(b.TableName()).Where("id = ?", b.ID).Updates(fields)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

func (b *Book) Delete() error {
	db := dao.DB(b.TableName()).Where("id = ?", b.ID).Delete(b)
	if db.Error != nil {
		return dao.ErrDBServer
	}
	return nil
}

func updateFields(b *Book) (fields map[string]interface{}) {
	fields = make(map[string]interface{})
	if b.Title != "" {
		fields["title"] = b.Title
	}
	if b.Genre != "" {
		fields["genre"] = b.Genre
	}
	if b.Intro != "" {
		fields["intro"] = b.Intro
	}
	return fields
}

R:查询

实现查询之前,我们先定义两个查询Handler interface,创建文件 ${ROOT}/query/itf.go,编写一下代码:

package query

import (
	"github.com/elzatahmed/go-gorm/model/author"
	"github.com/elzatahmed/go-gorm/model/book"
)

type AuthorHandler interface {
	FindByID(ID uint) (auth *author.Author, err error)
	FindAllByName(name string) (authors []*author.Author, err error)
	FindAllByAgeLessThan(age uint) (authors []*author.Author, err error)
	FindAllByMaleAuthor() (authors []*author.Author, err error)
	FindAllFemaleAuthor() (authors []*author.Author, err error)
}

type BookHandler interface {
	FindByID(ID uint) (b *book.Book, err error)
	FindAllByTitle(title string) (books []*book.Book, err error)
	FindAllByGenre(genre string) (books []*book.Book, err error)
	FindAllByAuthorID(authorID uint) (books []*book.Book, err error)
}

我们在这里定义了我们需要实现的所有查询方法,接下来我们就要实现他。

创建文件 ${ROOT}/query/impl.go,在内部定义结构体 AuthorHandlerImpl,让该结构体实现 AuthorHandler 的所有方法。接触过go接口编程的同学可能发现了go在实现interface的时候并没有像java那样implements的关键词,所以我们并不能用肉眼保证其一定实现了该接口,在编译阶段检查该行为的一种方式如下:

type AuthorHandlerImpl struct{}

// 这种方式并不会创建新的变量,因为变量已被 _ 忽略
// 但是如果AuthorHandlerImpl并没有实现接口AuthorHandler,这个表达式在编译期间就不会通过
var _ AuthorHandler = (*AuthorHandlerImpl)(nil)

接下来的任务就是要实现所有方法:

func (a AuthorHandlerImpl) FindByID(ID uint) (auth *author.Author, err error) {
	db := dao.DB(dao.TableNameAuthor).Where("id = ?", ID).First(auth)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (a AuthorHandlerImpl) FindAllByName(name string) (authors []*author.Author, err error) {
	db := dao.DB(dao.TableNameAuthor).Where("name LIKE ?", "%"+name).Find(authors)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (a AuthorHandlerImpl) FindAllByAgeLessThan(age uint) (authors []*author.Author, err error) {
	db := dao.DB(dao.TableNameAuthor).Where("age < ?", age).Find(authors)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (a AuthorHandlerImpl) FindAllMaleAuthor() (authors []*author.Author, err error) {
	db := dao.DB(dao.TableNameAuthor).Where("gender = ?", author.GenderMale).Find(authors)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (a AuthorHandlerImpl) FindAllFemaleAuthor() (authors []*author.Author, err error) {
	db := dao.DB(dao.TableNameAuthor).Where("gender = ?", author.GenderFemale).Find(authors)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

在阅读上述代码时你会发现这几个方法的逻辑都几乎一模一样,唯一不一样的一点就是gorm的使用方法。大家需要学习的就是这里gorm的链式调用的使用方法。如果对我的错误处理方式有感兴趣的可以阅读文档 Golang中的错误处理

接下来,以同样的方式去实现 BookHandlerImpl

type BookHandlerImpl struct {}

var _ BookHandler = (*BookHandlerImpl)(nil)

func (bk BookHandlerImpl) FindByID(ID uint) (b *book.Book, err error) {
	db := dao.DB(dao.TableNameBook).Where("id = ?", ID).First(b)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (bk BookHandlerImpl) FindAllByTitle(title string) (books []*book.Book, err error) {
	db := dao.DB(dao.TableNameBook).Where("title LIKE ?", "%"+title).Find(books)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (bk BookHandlerImpl) FindAllByGenre(genre string) (books []*book.Book, err error) {
	db := dao.DB(dao.TableNameBook).Where("genre = ?", genre).Find(books)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

func (bk BookHandlerImpl) FindAllByAuthorID(authorID uint) (books []*book.Book, err error) {
	db := dao.DB(dao.TableNameBook).Where("author_id = ?", authorID).Find(books)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, dao.ErrDBNoRecord
		}
		return nil, dao.ErrDBServer
	}
	return
}

还剩最后一步操作,即我们需要为interface AuthorHandlerBookHandler 编写构造函数:

func NewAuthorHandler() AuthorHandler {
	return AuthorHandlerImpl{}
}

func NewBookHandler() BookHandler {
	return BookHandlerImpl{}
}

这样我们所有的数据库操作模型已建立完成!

主函数

读到这不知道你还记不记得我们的所有初始化操作是用yaml文件的解析来完成的,所以我们首先需要编写我们的yaml文件:

${ROOT}/conf/db.yaml

dialect: "mysql"
username: "root"
password: "*****"
name: "db"
host: "localhost"
port: "3306"
query: "useSSL=true"
debugMode: true

我们再在main中去解析yaml文件并传入到初始化函数中:

func main() {
	dbConf := loadConfig("conf")
	dao.MustInit(dbConf)
}

func loadConfig(configDir string) *conf.DB {
	var (
		dbConf *conf.DB
	)
	db, err := os.Open(configDir + "/db.yaml")
	if err != nil {
		panic(err)
	}
	if yaml.NewDecoder(db).Decode(dbConf) != nil {
		panic(err)
	}
	return dbConf
}

这样初始化完成后,我们就可以在main中执行我们的CRUD:

func main() {
	dbConf := loadConfig("conf")
	dao.MustInit(dbConf)

	martin := author.New("George.R.R.Martin", author.GenderMale, 60)
	err := martin.Save()
	if err != nil {
		panic(err)
	}
	martin.Age = 70
	err = martin.Update()
	if err != nil {
		panic(err)
	}
	err = martin.Delete()
	if err != nil {
		panic(err)
	}
	authorHandler := query.NewAuthorHandler()
	authors, err := authorHandler.FindAllByName("George")
	if err != nil {
		panic(err)
	}
	fmt.Println(authors)
}
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Go 语言中,最受欢迎的 ORM(Object-Relational Mapping)框架可能是 GormGorm 是一个简单而强大的 ORM 框架,支持许多数据库,包括 MySQL、PostgreSQL、SQLite 和 SQL Server。它具有高度可配置的查询构建器,允许您使用简单的方法查询数据库。此外,Gorm 还提供了许多有用的功能,如自动迁移、钩子函数和缓存支持。 另一个受欢迎的 Go 语言 ORM 框架是 Beego ORM。Beego ORM 是与 Beego 框架一起使用的 ORM,但也可以单独使用。它提供了简单易用的 API,允许您在 Go 中轻松地操作数据库。Beego ORM 还提供了许多有用的功能,如自动迁移、模型钩子函数和查询构建器支持。 还有许多其他优秀的 Go 语言 ORM 框架,如 SQLBoiler、Xorm 和 buf clean。最终,哪个 ORM 框架最适合您取决于您的需求和喜好。建议您比较多个选项,然后根据您的项目需求选择最合适的 ORM 框架。 ### 回答2: 在Go语言中,有许多流行的ORM(对象关系映射)框架可供选择,而最好用的框架往往会因个人需求而异。然而,其中一个备受推崇的是GORMGORM是一个功能强大的Go语言ORM框架,被广泛认可的原因有以下几点: 1. 简单易用:GORM提供了简洁直观的API,使得对数据库的操作变得轻而易举。开发者可以利用GORM实现复杂的数据库查询、插入、更新和删除操作,而无需深入学习SQL语句。 2. 数据库兼容性:GORM支持多种数据库,如MySQL、PostgreSQL、SQLite等,因此具备良好的数据库兼容性。这使得GORM可以适用于各种类型的项目,无论是小型还是大型,无论是简单还是复杂。 3. 数据迁移工具:GORM内置了强大的数据库迁移工具,可以轻松地管理数据库模式的变更。通过简单的命令,开发者可以自动执行数据库迁移脚本,而无需手动操作数据库。 4. 提供事务支持:GORM支持事务处理,使得开发者可以对数据库操作进行原子性处理。通过提供Begin、Commit和Rollback等事务相关的方法,GORM确保了数据的完整性和一致性。 5. 强大的查询构建器:GORM的查询构建器使得开发者可以快速构建复杂的查询语句,包括联接查询、条件筛选、分组、排序等。同时,GORM还支持预加载机制,减少了数据库查询的次数,提高了性能。 总而言之,GORM作为一种功能强大、易用性高、数据库兼容性好的ORM框架,可以帮助开发者更加便捷地操作数据库,提高开发效率和代码质量。因此,GORM可以被认为是Go语言中最好用的ORM框架之一。 ### 回答3: Go语言最好用的ORM框架GORMGORM是一个功能丰富且易于使用的ORM库,提供了非常方便的数据库访问和操作接口,可以与多种数据库系统进行交互。以下是GORM的一些特点: 1. 简单易用:GORM提供了简洁的API和强大的查询功能,使得开发人员可以更轻松地进行数据库操作。 2. 自动迁移:GORM可以自动将Go结构体与数据库表进行映射,并可以根据结构体的变化自动更新数据库表结构,减少了手动迁移的工作。 3. 事务支持:GORM支持事务操作,开发人员可以使用事务来保证数据库操作的一致性和完整性。 4. 关联关系映射:GORM支持一对一、一对多、多对多等关联关系的映射,可以轻松进行复杂的数据查询和操作。 5. 高性能:GORM通过一系列的优化措施,如缓存、批量操作等,提供了较高的性能。 6. 扩展性:GORM提供了丰富的Hook机制,可以方便地在数据库操作中插入自定义逻辑,扩展其功能。 总之,GORM是一个功能全面、易于使用且性能优越的ORM框架,它为Go语言的开发者提供了便捷的数据库操作解决方案,是最好用的ORM框架之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值