Golang ORM框架 — GORM

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

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

gorm

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

数据库配置模型

${ROOT}/model/confdb.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模式
)
InitMustInitpanic
// 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)
	}
}
gormOpengorm.OpenDialectDSNgorm.DBdb
model/conf/db.go
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.Table
gorm.DB
db.Opendb.Debug()db.Tablegorm.DBgorm.DBdb.Debug
// Debug start debug mode
func (s *DB) Debug() *DB {
	return s.clone().LogMode(true)
}
s.clone().LogMode(true)dbdbdb

模型定义

gormgorm.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"`
}
gogormgormDeletedAtDeletedAt

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

作者-书籍模型

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

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
}
gormgormgormgorm
标签名描述
column列名
type类型
size数据大小/长度
primary_key主键标识
unique唯一键标识
not null不能为空
auto_increment自增记录

CRUD

handler
gorm
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.Wheredb.Where("id = ?", id)
db.Wheredb.Ordb.Not
db.Firstdb.Finddb.Firstdb.Finddb.Firstdb.Find
db.Firstdb.Finddb.Where
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.Updatesdb.Updatedb.Updatesmapstruct

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:查询

${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.goAuthorHandlerImplAuthorHandler
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
}
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)
}