互联网开发最重要的一部分就是与数据库的交互,该部分在我们分层互联网模型中会归属于 — 模型层 + 仓库层。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)
}