阿里云网站备案,微信小程序公众号开发,网页布局的常用方法,如何建立搜索功能的网站目前#xff0c;GitHub上 star数最多的是GORM#xff0c;它也是当前Go项目中使用最多的ORM。 GORM基础知识介绍
GORM是Go语言的ORM包#xff0c;功能强大#xff0c;调用方便。像腾讯、华为、阿里这样的大厂#xff0c;都在使用GORM来构建企业级的应用。
功能全。使用O… 目前GitHub上 star数最多的是GORM它也是当前Go项目中使用最多的ORM。 GORM基础知识介绍
GORM是Go语言的ORM包功能强大调用方便。像腾讯、华为、阿里这样的大厂都在使用GORM来构建企业级的应用。
功能全。使用ORM操作数据库的接口GORM都有可以满足我们开发中对数据库调用的各类需求。支持钩子方法。这些钩子方法可以应用在Create、Save、Update、Delete、Find方法中。开发者友好调用方便。支持Auto Migration。支持关联查询。支持多种关系数据库例如MySQL、Postgres、SQLite、SQLServer等。
GORM有两个版本V1和V2。
通过示例学习GORM
接下来我们先快速看一个使用GORM的示例通过该示例来学习GORM。示例代码存放在marmotedu/gopractise-demo/gorm/main.go文件中。因为代码比较长你可以使用以下命令克隆到本地查看
$ mkdir -p $GOPATH/src/github.com/marmotedu
$ cd $GOPATH/src/github.com/marmotedu
$ git clone https://github.com/marmotedu/gopractise-demo
$ cd gopractise-demo/gorm/假设我们有一个MySQL数据库连接地址和端口为 127.0.0.1:3306 用户名为 iam 密码为 iam1234 。创建完main.go文件后执行以下命令来运行
$ go run main.go -H 127.0.0.1:3306 -u iam -p iam1234 -d test
2020/10/17 15:15:50 totalcount: 1
2020/10/17 15:15:50 code: D42, price: 100
2020/10/17 15:15:51 totalcount: 1
2020/10/17 15:15:51 code: D42, price: 200
2020/10/17 15:15:51 totalcount: 0在企业级Go项目开发中使用GORM库主要用来完成以下数据库操作
连接和关闭数据库。连接数据库时可能需要设置一些参数比如最大连接数、最大空闲连接数、最大连接时长等。插入表记录。可以插入一条记录也可以批量插入记录。更新表记录。可以更新某一个字段也可以更新多个字段。查看表记录。可以查看某一条记录也可以查看符合条件的记录列表。删除表记录。可以删除某一个记录也可以批量删除。删除还支持永久删除和软删除。
GORM功能强大上面的示例代码展示的是比较通用的一种操作方式。
上述代码中首先定义了一个GORM模型Models Models定义如下
type Product struct {gorm.ModelCode string gorm:column:codePrice uint gorm:column:price
}// TableName maps to mysql table name.
func (p *Product) TableName() string {return product
}如果没有指定表名则GORM使用结构体名的蛇形复数作为表名。例如结构体名为 DockerInstance 则表名为 dockerInstances 。
在之后的代码中使用Pflag来解析命令行的参数通过命令行参数指定数据库的地址、用户名、密码和数据库名。之后使用这些参数生成建立 MySQL 连接需要的配置文件并调用 gorm.Open 建立数据库连接
var (host pflag.StringP(host, H, 127.0.0.1:3306, MySQL service host address)username pflag.StringP(username, u, root, Username for access to mysql service)password pflag.StringP(password, p, root, Password for access to mysql, should be used pair with password)database pflag.StringP(database, d, test, Database name to use)help pflag.BoolP(help, h, false, Print this help message)
)func main() {// Parse command line flagspflag.CommandLine.SortFlags falsepflag.Usage func() {pflag.PrintDefaults()}pflag.Parse()if *help {pflag.Usage()return}dns : fmt.Sprintf(%s:%stcp(%s)/%s?charsetutf8parseTime%tloc%s,*username,*password,*host,*database,true,Local)db, err : gorm.Open(mysql.Open(dns), gorm.Config{})if err ! nil {panic(failed to connect database)}
}创建完数据库连接之后会返回数据库实例 db 之后就可以调用db实例中的方法完成数据库的CURD操作。
第一个操作自动迁移表结构。
// 1. Auto migration for given models
db.AutoMigrate(Product{})我不建议你在正式的代码中自动迁移表结构。
GORM的AutoMigrate方法只对新增的字段或索引进行变更理论上是没有风险的。在实际的Go项目开发中也有很多人使用AutoMigrate方法自动同步表结构
第二个操作插入表记录。
// 2. Insert the value into database
if err : db.Create(Product{Code: D42, Price: 100}).Error; err ! nil {log.Fatalf(Create error: %v, err)
}
PrintProducts(db)通过 db.Create 方法创建了一条记录。插入记录后通过调用 PrintProducts 方法打印当前表中的所有数据记录来测试是否成功插入。
第三个操作获取符合条件的记录。
// 3. Find first record that match given conditions
product : Product{}
if err : db.Where(code ?, D42).First(product).Error; err ! nil {log.Fatalf(Get product error: %v, err)
}第四个操作更新表记录。
// 4. Update value in database, if the value doesnt have primary key, will insert it
product.Price 200
if err : db.Save(product).Error; err ! nil {log.Fatalf(Update product error: %v, err)
}
PrintProducts(db)通过Save方法可以把 product 变量中所有跟数据库不一致的字段更新到数据库中。具体操作是先获取某个资源的详细信息再通过 product.Price 200 这类赋值语句对其中的一些字段重新赋值。
第五个操作删除表记录。
通过 Delete 方法删除表记录代码如下
// 5. Delete value match given conditions
if err : db.Where(code ?, D42).Delete(Product{}).Error; err ! nil {log.Fatalf(Delete product error: %v, err)
}
PrintProducts(db)这里需要注意因为 Product 中有 gorm.DeletedAt 字段所以上述删除操作不会真正把记录从数据库表中删除掉而是通过设置数据库 product 表 deletedAt 字段为当前时间的方法来删除。
第六个操作获取表记录列表。
products : make([]*Product, 0)
var count int64
d : db.Where(code like ?, %D%).Offset(0).Limit(2).Order(id desc).Find(products).Offset(-1).Limit(-1).Count(count)
if d.Error ! nil {log.Fatalf(List products error: %v, d.Error)
}在PrintProducts函数中会打印当前的所有记录。
GORM常用操作讲解 模型定义
GORM使用模型Models来映射一个数据库表。默认情况下使用ID作为主键使用结构体名的 snake_cases 作为表名使用字段名的 snake_case 作为列名并使用 CreatedAt、UpdatedAt、DeletedAt字段追踪创建、更新和删除时间。
使用GORM的默认规则可以减少代码量但我更喜欢的方式是直接指明字段名和表名。例如有以下模型
type Animal struct {AnimalID int64 // 列名 animal_idBirthday time.Time // 列名 birthdayAge int64 // 列名 age
}上述模型对应的表名为 animals 列名分别为 animal_id 、 birthday 和 age 。我们可以通过以下方式来重命名表名和列名并将 AnimalID 设置为表的主键
type Animal struct {AnimalID int64 gorm:column:animalID;primarykey // 将列名设为 animalIDBirthday time.Time gorm:column:birthday // 将列名设为 birthdayAge int64 gorm:column:age // 将列名设为 age
}func (a *Animal) TableName() string {return animal
}上面的代码中通过 primaryKey 标签指定主键使用 column 标签指定列名通过给Models添加 TableName 方法指定表名。
数据库表通常会包含4个字段。
ID自增字段也作为主键。CreatedAt记录创建时间。UpdatedAt记录更新时间。DeletedAt记录删除时间软删除时有用。
GORM也预定义了包含这4个字段的Models 例如
type Animal struct {gorm.ModelAnimalID int64 gorm:column:animalID // 将列名设为 animalIDBirthday time.Time gorm:column:birthday // 将列名设为 birthdayAge int64 gorm:column:age // 将列名设为 age
}Models中的字段能支持很多GORM标签但如果我们不使用GORM自动创建表和迁移表结构的功能很多标签我们实际上是用不到的。 用得最多的是 column 标签。
连接数据库
在进行数据库的CURD操作之前我们首先需要连接数据库。你可以通过以下代码连接MySQL数据库
import (gorm.io/driver/mysqlgorm.io/gorm
)func main() {// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情dsn : user:passtcp(127.0.0.1:3306)/dbname?charsetutf8mb4parseTimeTruelocLocaldb, err : gorm.Open(mysql.Open(dsn), gorm.Config{})
}如果需要GORM正确地处理 time.Time 类型在连接数据库时需要带上 parseTime 参数。如果要支持完整的UTF-8编码可将charsetutf8更改为charsetutf8mb4。
GORM支持连接池底层是用 database/sql 包来维护连接池的连接池设置如下
sqlDB, err : db.DB()
sqlDB.SetMaxIdleConns(10) // 设置MySQL的最大空闲连接数推荐100
sqlDB.SetMaxOpenConns(100) // 设置MySQL的最大连接数推荐100
sqlDB.SetConnMaxLifetime(time.Hour) // 设置MySQL的空闲连接最大存活时间推荐10s创建记录
我们可以通过 db.Create 方法来创建一条记录
type User struct {gorm.ModelName stringAge uint8Birthday *time.Time
}
user : User{Name: Jinzhu, Age: 18, Birthday: time.Now()}
result : db.Create(user) // 通过数据的指针来创建db.Create函数会返回如下3个值
user.ID返回插入数据的主键这个是直接赋值给user变量。result.Error返回error。result.RowsAffected返回插入记录的条数。
当需要插入的数据量比较大时可以批量插入以提高插入性能
var users []User{{Name: jinzhu1}, {Name: jinzhu2}, {Name: jinzhu3}}
DB.Create(users)for _, user : range users {user.ID // 1,2,3
}删除记录
我们可以通过Delete方法删除记录
// DELETE from users where id 10 AND name jinzhu;
db.Where(name ?, jinzhu).Delete(User{})GORM也支持根据主键进行删除例如
// DELETE FROM users WHERE id 10;
db.Delete(User{}, 10)不过我更喜欢使用db.Where的方式进行删除这种方式有两个优点。
第一个优点是删除方式更通用。使用db.Where不仅可以根据主键删除还能够随意组合条件进行删除。
第二个优点是删除方式更显式这意味着更易读。如果使用db.Delete(User{}, 10)你还需要确认User的主键如果记错了主键还可能会引入Bug。
此外GORM也支持批量删除
db.Where(name in (?), []string{jinzhu, colin}).Delete(User{})GORM支持两种删除方法软删除和永久删除。下面我来分别介绍下。
软删除
软删除是指执行Delete时记录不会被从数据库中真正删除。GORM会将 DeletedAt 设置为当前时间并且不能通过正常的方式查询到该记录。如果模型包含了一个 gorm.DeletedAt 字段GORM在执行删除操作时会软删除该记录。
下面的删除方法就是一个软删除
// UPDATE users SET deleted_at2013-10-29 10:23 WHERE age 20;
db.Where(age ?, 20).Delete(User{})// SELECT * FROM users WHERE age 20 AND deleted_at IS NULL;
db.Where(age 20).Find(user)我们可以通过下面的方式查找被软删除的记录
// SELECT * FROM users WHERE age 20;
db.Unscoped().Where(age 20).Find(users)永久删除
如果想永久删除一条记录可以使用Unscoped
// DELETE FROM orders WHERE id10;
db.Unscoped().Delete(order)或者你也可以在模型中去掉gorm.DeletedAt。
更新记录
GORM中最常用的更新方法如下
db.First(user)user.Name jinzhu 2
user.Age 100
// UPDATE users SET namejinzhu 2, age100, birthday2016-01-01, updated_at 2013-11-17 21:34:10 WHERE id111;
db.Save(user)还可以指定更新单个列
// UPDATE users SET age200, updated_at2013-11-17 21:34:10 WHERE namecolin;
db.Model(User{}).Where(name ?, colin).Update(age, 200)也可以指定更新多个列
// UPDATE users SET namehello, age18, updated_at 2013-11-17 21:34:10 WHERE name colin;
db.Model(user).Where(name, colin).Updates(User{Name: hello, Age: 18, Active: false})这个方法只会更新非零值的字段。
查询数据
GORM支持不同的查询方法 分别是检索单个记录、查询所有符合条件的记录和智能选择字段。
检索单个记录
下面是检索单个记录的示例代码
// 获取第一条记录主键升序
// SELECT * FROM users ORDER BY id LIMIT 1;
db.First(user)// 获取最后一条记录主键降序
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
db.Last(user)
result : db.First(user)
result.RowsAffected // 返回找到的记录数
result.Error // returns error// 检查 ErrRecordNotFound 错误
errors.Is(result.Error, gorm.ErrRecordNotFound)如果model类型没有定义主键则按第一个字段排序。
查询所有符合条件的记录
示例代码如下
users : make([]*User, 0)// SELECT * FROM users WHERE name jinzhu;
db.Where(name ?, jinzhu).Find(users)智能选择字段
你可以通过Select方法选择特定的字段。我们可以定义一个较小的结构体来接受选定的字段
type APIUser struct {ID uintName string
}// SELECT id, name FROM users LIMIT 10;
db.Model(User{}).Limit(10).Find(APIUser{})除了上面讲的三种常用的基本查询方法GORM还支持高级查询下面我来介绍下。
高级查询
GORM支持很多高级查询功能这里我主要介绍4种。
指定检索记录时的排序方式
示例代码如下
// SELECT * FROM users ORDER BY age desc, name;
db.Order(age desc, name).Find(users)Limit Offset
Offset指定从第几条记录开始查询Limit指定返回的最大记录数。Offset和Limit值为-1时消除Offset和Limit条件。另外Limit和Offset位置不同效果也不同。
// SELECT * FROM users OFFSET 5 LIMIT 10;
db.Limit(10).Offset(5).Find(users)Distinct
Distinct可以从数据库记录中选择不同的值。
db.Distinct(name, age).Order(name, age desc).Find(results)Count
Count可以获取匹配的条数。
var count int64
// SELECT count(1) FROM users WHERE name jinzhu; (count)
db.Model(User{}).Where(name ?, jinzhu).Count(count)GORM还支持很多高级查询功能比如内联条件、Not 条件、Or 条件、Group Having、Joins、Group、FirstOrInit、FirstOrCreate、迭代、FindInBatches等。 可以看下GORM的官方文档。
原生SQL
GORM支持原生查询SQL和执行SQL。原生查询SQL用法如下
type Result struct {ID intName stringAge int
}var result Result
db.Raw(SELECT id, name, age FROM users WHERE name ?, 3).Scan(result)原生执行SQL用法如下
db.Exec(DROP TABLE users)
db.Exec(UPDATE orders SET shipped_at? WHERE id IN ?, time.Now(), []int64{1,2,3})GORM钩子
GORM支持钩子功能例如下面这个在插入记录前执行的钩子
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {u.UUID uuid.New()if u.Name admin {return errors.New(invalid name)}return
}GORM支持的钩子见下表 iam-apiserver中的CURD操作实战
接下来我来介绍下iam-apiserver是如何使用GORM对数据进行CURD操作的。
首先我们需要配置连接MySQL的各类参数。iam-apiserver通过NewMySQLOptions函数创建了一个带有默认值的MySQLOptions类型的变量将该变量传给NewApp函数。在App框架中最终会调用MySQLOptions提供的AddFlags方法将MySQLOptions提供的命令行参数添加到Cobra命令行中。
接着在PrepareRun函数中调用GetMySQLFactoryOr函数初始化并获取仓库层的实例mysqlFactory。实现了仓库层store.Factory接口
type Factory interface {Users() UserStoreSecrets() SecretStorePolicies() PolicyStoreClose() error
}GetMySQLFactoryOr函数采用了我们在 11讲 中提过的单例模式确保iam-apiserver进程中只有一个仓库层的实例这样可以减少内存开支和系统的性能开销。
GetMySQLFactoryOr函数中使用github.com/marmotedu/iam/pkg/db包提供的New函数创建了MySQL实例。New函数代码如下
func New(opts *Options) (*gorm.DB, error) { dns : fmt.Sprintf(%s:%stcp(%s)/%s?charsetutf8parseTime%tloc%s, opts.Username, opts.Password, opts.Host, opts.Database, true, Local) db, err : gorm.Open(mysql.Open(dns), gorm.Config{ Logger: logger.New(opts.LogLevel), }) if err ! nil { return nil, err } sqlDB, err : db.DB() if err ! nil { return nil, err } // SetMaxOpenConns sets the maximum number of open connections to the database.sqlDB.SetMaxOpenConns(opts.MaxOpenConnections)// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.sqlDB.SetConnMaxLifetime(opts.MaxConnectionLifeTime)// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.sqlDB.SetMaxIdleConns(opts.MaxIdleConnections)return db, nil
}上述代码中我们先创建了一个 *gorm.DB 类型的实例并对该实例进行了如下设置
通过SetMaxOpenConns方法设置了MySQL的最大连接数推荐100。通过SetConnMaxLifetime方法设置了MySQL的空闲连接最大存活时间推荐10s。通过SetMaxIdleConns方法设置了MySQL的最大空闲连接数推荐100。
GetMySQLFactoryOr函数最后创建了datastore类型的变量mysqlFactory该变量是仓库层的变量。mysqlFactory变量中又包含了 *gorm.DB 类型的字段 db 。
最终我们通过仓库层的变量mysqlFactory调用其 db 字段提供的方法来完成数据库的CURD操作。例如创建密钥、更新密钥、删除密钥、获取密钥详情、查询密钥列表具体代码如下代码位于secret.go文件中
// Create creates a new secret.
func (s *secrets) Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) error {return s.db.Create(secret).Error
}// Update updates an secret information by the secret identifier.
func (s *secrets) Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) error {return s.db.Save(secret).Error
}// Delete deletes the secret by the secret identifier.
func (s *secrets) Delete(ctx context.Context, username, name string, opts metav1.DeleteOptions) error {if opts.Unscoped {s.db s.db.Unscoped()}err : s.db.Where(username ? and name ?, username, name).Delete(v1.Secret{}).Errorif err ! nil !errors.Is(err, gorm.ErrRecordNotFound) {return errors.WithCode(code.ErrDatabase, err.Error())}return nil
}// Get return an secret by the secret identifier.
func (s *secrets) Get(ctx context.Context, username, name string, opts metav1.GetOptions) (*v1.Secret, error) {secret : v1.Secret{}err : s.db.Where(username ? and name ?, username, name).First(secret).Errorif err ! nil {if errors.Is(err, gorm.ErrRecordNotFound) {return nil, errors.WithCode(code.ErrSecretNotFound, err.Error())}return nil, errors.WithCode(code.ErrDatabase, err.Error())}return secret, nil
}// List return all secrets.
func (s *secrets) List(ctx context.Context, username string, opts metav1.ListOptions) (*v1.SecretList, error) {ret : v1.SecretList{}ol : gormutil.Unpointer(opts.Offset, opts.Limit)if username ! {s.db s.db.Where(username ?, username)}selector, _ : fields.ParseSelector(opts.FieldSelector)name, _ : selector.RequiresExactMatch(name)d : s.db.Where( name like ?, %name%).Offset(ol.Offset).Limit(ol.Limit).Order(id desc).Find(ret.Items).Offset(-1).Limit(-1).Count(ret.TotalCount)return ret, d.Error
}上面的代码中 s.db 就是 *gorm.DB 类型的字段。
上面的代码段执行了以下操作
通过 s.db.Save 来更新数据库表的各字段通过 s.db.Unscoped 来永久性从表中删除一行记录。对于支持软删除的资源我们还可以通过 opts.Unscoped 选项来控制是否永久删除记录。 true 永久删除 false 软删除默认软删除。通过 errors.Is(err, gorm.ErrRecordNotFound) 来判断GORM返回的错误是否是没有找到记录的错误类型。通过下面两行代码来获取查询条件name的值
selector, _ : fields.ParseSelector(opts.FieldSelector)
name, _ : selector.RequiresExactMatch(name)我们的整个调用链是控制层 - 业务层 - 仓库层。这里你可能要问我们是如何调用到仓库层的实例mysqlFactory的呢
这是因为我们的控制层实例包含了业务层的实例。在创建控制层实例时我们传入了业务层的实例
type UserController struct { srv srvv1.Service
} // NewUserController creates a user handler.
func NewUserController(store store.Factory) *UserController {return UserController{ srv: srvv1.NewService(store), }
} 业务层的实例包含了仓库层的实例。在创建业务层实例时传入了仓库层的实例
type service struct { store store.Factory
} // NewService returns Service interface.
func NewService(store store.Factory) Service { return service{ store: store, }
}通过这种包含关系我们在控制层可以调用业务层的实例在业务层又可以调用仓库层的实例。这样我们最终通过仓库层实例的 db 字段*gorm.DB 类型完成数据库的CURD操作。
总结
在Go项目中我们需要使用ORM来进行数据库的CURD操作。在Go生态中当前最受欢迎的ORM是GORMIAM项目也使用了GORM。 这些常用功能的常见使用方式如下
package mainimport (fmtloggithub.com/spf13/pflaggorm.io/driver/mysqlgorm.io/gorm
)type Product struct {gorm.ModelCode string gorm:column:codePrice uint gorm:column:price
}// TableName maps to mysql table name.
func (p *Product) TableName() string {return product
}var (host pflag.StringP(host, H, 127.0.0.1:3306, MySQL service host address)username pflag.StringP(username, u, root, Username for access to mysql service)password pflag.StringP(password, p, root, Password for access to mysql, should be used pair with password)database pflag.StringP(database, d, test, Database name to use)help pflag.BoolP(help, h, false, Print this help message)
)func main() {// Parse command line flagspflag.CommandLine.SortFlags falsepflag.Usage func() {pflag.PrintDefaults()}pflag.Parse()if *help {pflag.Usage()return}dns : fmt.Sprintf(%s:%stcp(%s)/%s?charsetutf8parseTime%tloc%s,*username,*password,*host,*database,true,Local)db, err : gorm.Open(mysql.Open(dns), gorm.Config{})if err ! nil {panic(failed to connect database)}// 1. Auto migration for given modelsdb.AutoMigrate(Product{})// 2. Insert the value into databaseif err : db.Create(Product{Code: D42, Price: 100}).Error; err ! nil {log.Fatalf(Create error: %v, err)}PrintProducts(db)// 3. Find first record that match given conditionsproduct : Product{}if err : db.Where(code ?, D42).First(product).Error; err ! nil {log.Fatalf(Get product error: %v, err)}// 4. Update value in database, if the value doesnt have primary key, will insert itproduct.Price 200if err : db.Save(product).Error; err ! nil {log.Fatalf(Update product error: %v, err)}PrintProducts(db)// 5. Delete value match given conditionsif err : db.Where(code ?, D42).Delete(Product{}).Error; err ! nil {log.Fatalf(Delete product error: %v, err)}PrintProducts(db)
}// List products
func PrintProducts(db *gorm.DB) {products : make([]*Product, 0)var count int64d : db.Where(code like ?, %D%).Offset(0).Limit(2).Order(id desc).Find(products).Offset(-1).Limit(-1).Count(count)if d.Error ! nil {log.Fatalf(List products error: %v, d.Error)}log.Printf(totalcount: %d, count)for _, product : range products {log.Printf(\tcode: %s, price: %d\n, product.Code, product.Price)}
}此外GORM还支持原生查询SQL和原生执行SQL可以满足更加复杂的SQL场景。
GORM中还有一个非常有用的功能是支持Hooks。Hooks可以在执行某个CURD操作前被调用。在Hook中可以添加一些非常有用的功能例如生成唯一ID。目前GORM支持 BeforeXXX 、 AfterXXX 和 AfterFind Hook其中 XXX 可以是 Save、Create、Delete、Update。
对标java的mybatis的功能常见的sql功能都支持对比学习是快速入门的法宝。
课后练习
GORM支持AutoMigrate功能思考下你的生产环境是否可以使用AutoMigrate功能为什么查看GORM官方文档看下如何用GORM实现事务回滚功能。