全球短讯!Go开源世界主流成熟ORM框架gorm实践分享

2023-05-17 04:34:32 来源:博客园

@

目录概述定义核心功能声明模型与约定gorm.Model字段级权限时间惯例嵌入结构字段标签使用安装数据库链接连接池CRUD 接口创建查询高级查询修改删除原始SQL事务转换分片序列化概述定义

GORM 官网地址 https://gorm.io/ 最新版本v1.25.1

GORM 官网文档地址 https://gorm.io/docs/


【资料图】

GORM 源码地址 https://github.com/go-gorm/gorm

GORM 是Golang语言中一个功能齐全的优秀的ORM 框架,对开发者友好,支持多种数据库,并提供了丰富的功能和 API,可以让开发者更加方便地进行数据库操作。

核心功能ORM功能丰富、完整关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)Create,Save,Update,Delete,Find 中钩子方法支持 PreloadJoins的预加载事务,嵌套事务,Save Point,Rollback To Saved PointContext、预编译模式、DryRun 模式批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUDSQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询复合主键,索引,约束Auto Migration自定义 Logger灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…每个特性都经过了测试的重重考验开发者友好声明模型与约定

模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner和 Valuer接口的自定义类型及其指针或别名组成,示例如:

type User struct {  ID           uint  Name         string  Email        *string  Age          uint8  Birthday     *time.Time  MemberNumber sql.NullString  ActivatedAt  sql.NullTime  CreatedAt    time.Time  UpdatedAt    time.Time}

GORM 倾向于约定优于配置 默认情况下,GORM 使用 ID作为主键,使用结构体名的 蛇形复数作为表名,字段名的 蛇形作为列名,并使用 CreatedAtUpdatedAt字段追踪创建、更新时间。如果遵循GORM采用的约定,则只需编写很少的配置/代码。如果约定不符合需求,GORM也允许指定配置。

gorm.Model

GORM定义了gorm.Model,其中包括字段ID, CreatedAt, UpdatedAt, DeletedAt,可以将其嵌入到结构中以包含这些字段

字段级权限

可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略

type User struct {  Name string `gorm:"<-:create"` // 允许读和创建  Name string `gorm:"<-:update"` // 允许读和更新  Name string `gorm:"<-"`        // 允许读和写(创建和更新)  Name string `gorm:"<-:false"`  // 允许读,禁止写  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)  Name string `gorm:"->;<-:create"` // 允许读和写  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)  Name string `gorm:"-"`  // 通过 struct 读写会忽略该字段  Name string `gorm:"-:all"`        // 通过 struct 读写、迁移会忽略该字段  Name string `gorm:"-:migration"`  // 通过 struct 迁移会忽略该字段}
时间惯例

GORM按惯例使用CreatedAt、UpdatedAt来跟踪创建/更新时间,如果定义了字段,GORM将在创建/更新时设置当前时间。要使用具有不同名称的字段,可以使用标签autoCreateTime、autoUpdateTime来配置这些字段。如果希望节省存储不采用时间格式改为采用UNIX(毫/纳)秒,可以简单地更改字段的数据类型。

type User struct {  CreatedAt time.Time // 如果创建时为零,则设置为当前时间  UpdatedAt int       // 在更新时设置为当前unix秒数,或者在创建时设置为零  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用unix纳秒作为更新时间  Updated   int64 `gorm:"autoUpdateTime:milli"`// 使用unix毫秒作为更新时间  Created   int64 `gorm:"autoCreateTime"`      // 使用unix seconds作为创建时间}
嵌入结构

例如将Go内置gorm.Model结构体嵌入到User结构体里

type User struct {  gorm.Model  Name string}// 这个定义等价于上面type User struct {  ID        uint           `gorm:"primaryKey"`  CreatedAt time.Time  UpdatedAt time.Time  DeletedAt gorm.DeletedAt `gorm:"index"`  Name string}

但对于普通的struct字段,可以将其嵌入标签,例如:

type Author struct {  Name  string  Email string}type Blog struct {  ID      int  Author  Author `gorm:"embedded"`  Upvotes int32 }// 这个定义等价于上面Blog内嵌Authortype Blog struct {  ID    int64  Name  string  Email string  Upvotes  int32}

可以使用标签embeddedPrefix为嵌入字段的数据库名称添加前缀,例如:

type Blog struct {  ID      int  Author  Author `gorm:"embedded;embeddedPrefix:author_"`  Upvotes int32}// 这个定义等价于上面Blog内嵌Authortype Blog struct {  ID          int64  AuthorName  string  AuthorEmail string  Upvotes     int32}
字段标签
Tag NameDescription
column数据库表列名
type列数据类型,例如:bool, int, uint, float, string, time, bytes,这适用于所有数据库,并且可以与其他标签一起使用,如" not null ", " size ", " autoIncrement "…指定的数据库数据类型,如" varbinary(8) "也支持,当使用指定的数据库数据类型时,它需要是一个完整的数据库数据类型,例如: MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT
serializer指定如何将数据序列化和反序列化到db的序列化器, e.g: serializer:json/gob/unixtime
size指定列数据大小/长度, e.g: size:256
primaryKey指定列为主键
unique将列指定为唯一约束
default指定列默认值
precision指定列精度
scale指定列的比例
not null指定列为NOT NULL
autoIncrement指定列可自动递增
autoIncrementIncrement自动递增步长,控制连续列值之间的间隔
embedded嵌入字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时跟踪当前时间,对于" int "字段,它将跟踪Unix秒,使用值" nano " / " milli "来跟踪Unix纳米/毫秒, e.g: autoCreateTime:nano
autoUpdateTime在创建/更新时跟踪当前时间,对于" int "字段,它将跟踪Unix秒,使用值" nano " / " milli "来跟踪Unix纳米/毫秒, e.g: autoUpdateTime:milli
index对多个字段使用相同的名称创建复合索引
uniqueIndex与" index "相同,但创建唯一的索引
check创建检查约束, eg: check:age > 13
<-设置字段的写权限," <-:create " create-only字段," <-:update " update-only字段," <-:false "无写权限," <- "创建和更新权限
->设置字段的读权限," ->:false "没有读权限
-忽略该字段," - "无读写权限," -:migration "无迁移权限," -:all "无读写迁移权限
comment在迁移时为字段添加注释
使用安装
# 引入gormgo get -u gorm.io/gorm# 引入mySQL驱动go get -u gorm.io/driver/mysql# 引入sqlite驱动go get -u gorm.io/driver/sqlite
数据库链接
import (  "gorm.io/driver/mysql"  "gorm.io/gorm")func main() {  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})}

MySQL Driver提供了一些可以在初始化时使用的高级配置,例如:

db, err := gorm.Open(mysql.New(mysql.Config{  DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // data source name  DefaultStringSize: 256, // default size for string fields  DisableDatetimePrecision: true, // disable datetime precision, which not supported before MySQL 5.6  DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB  DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB  SkipInitializeWithVersion: false, // auto configure based on currently MySQL version}), &gorm.Config{})
连接池

GORM使用数据库/sql维护连接池

sqlDB, err := db.DB()// SetMaxIdleConns设置空闲连接池中的最大连接数。sqlDB.SetMaxIdleConns(10)// SetMaxOpenConns设置数据库的最大打开连接数。sqlDB.SetMaxOpenConns(100)// SetConnMaxLifetime设置连接可能被重用的最大时间。sqlDB.SetConnMaxLifetime(time.Hour)
CRUD 接口创建
package mainimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm")type User struct {gorm.ModelName stringAge  int8}func main() {dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {fmt.Println("data error")}db.AutoMigrate(&User{})// 单个插入创建db.Create(&User{Name: "zhangsan", Age: 20})users := []User{{Name: "lisi", Age: 25},{Name: "wangwu", Age: 26},}// 多个插入并演示返回值result := db.Create(users)if result != nil {fmt.Println(result.RowsAffected)fmt.Println(result.Error)}var user User    // 根据主键ID查询主键值为1的记录并返回数据db.First(&user, 1)fmt.Println(user)}

MySQL的test数据库及对应表users信息如下:

// 指定批处理大小var users = []User{{Name: "jinzhu_1"}, ...., {Name: "jinzhu_10000"}}db.CreateInBatches(users, 100)// 初始化GORM时使用CreateBatchSize选项,所有INSERT在创建记录和关联时都将遵循此选项db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{  CreateBatchSize: 1000,})db := db.Session(&gorm.Session{CreateBatchSize: 1000})users = [5000]User{{Name: "jinzhu", Pets: []Pet{pet1, pet2, pet3}}...}db.Create(&users)

钩子函数示例

package mainimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm")type User struct {gorm.ModelName stringAge  int8}func (u *User) BeforeCreate(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("before create hint admin")}return}func (u *User) AfterCreate(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("after create hint admin")}return}func (u *User) BeforeSave(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("before save hint admin")}return}func (u *User) AfterSave(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("after save hint admin")}return}func main() {dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {fmt.Println("data error")}db.Create(&User{Name: "admin", Age: 40})}

如果你想跳过Hooks方法,可以使用SkipHooks会话模式

DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)
查询

GORM提供了First、Take、Last方法来从数据库中检索单个对象,它在查询数据库时添加了LIMIT 1条件,如果没有找到记录,它将返回错误ErrRecordNotFound。

First和Last方法将按主键顺序分别查找第一个和最后一个记录。只有当指向目标结构的指针作为参数传递给方法时,或者使用db.Model()指定模型时,它们才有效。此外,如果没有为相关模型定义主键,则模型将按第一个字段排序。

package mainimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm")type User struct {gorm.ModelName stringAge  int8}func (u *User) BeforeCreate(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("before create hint admin")}return}func (u *User) AfterCreate(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("after create hint admin")}return}func (u *User) BeforeSave(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("before save hint admin")}return}func (u *User) AfterSave(tx *gorm.DB) (err error) {if u.Name == "admin" {fmt.Println("after save hint admin")}return}func main() {dsn := "root:123456@tcp(192.168.50.95:3306)/test?charset=utf8&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {fmt.Println("data error")}var user, user1, user2, user3 User// 获取一条记录,下面相当于SELECT * FROM users ORDER BY id LIMIT 1;db.First(&user)fmt.Println("user=", user)// 获取一条记录, 下面相当于SELECT * FROM users LIMIT 1;db.Take(&user1)fmt.Println("user1=", user1)// 获取一条记录, 下面相当于SELECT * FROM users ORDER BY id DESC LIMIT 1;db.Last(&user2)fmt.Println(user2)// 指定表获取数据放入mapresult := map[string]interface{}{}db.Table("users").Take(&result)fmt.Println("result=", result)db.First(&user3, "id = ?", 3)fmt.Println("user3=", user3)var users, users1 []Userdb.Find(&users, []int{1, 2, 3})fmt.Println("users=", users)// 查询指定字段和条件db.Select("name").Where("id > ?", 2).Find(&users1)fmt.Println("users1=", users1)}

其他详细查看官网

// limit ,SELECT * FROM users LIMIT 3;db.Limit(3).Find(&users)// OFFSET ,SELECT * FROM users OFFSET 3;db.Offset(3).Find(&users)// group by ,SELECT name, sum(age) as total FROM `users` GROUP BY `name` HAVING name = "group"db.Model(&User{}).Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&result)// distinctdb.Distinct("name", "age").Order("name, age desc").Find(&results)// join ,SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.iddb.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})// scan,将结果扫描到结构体中的工作方式类似于我们使用Find的方式var result Resultdb.Table("users").Select("name", "age").Where("name = ?", "Antonio").Scan(&result)// 原始SQLdb.Raw("SELECT name, age FROM users WHERE name = ?", "Antonio").Scan(&result)
高级查询智能选择字段:GORM允许通过Select选择特定的字段,如果你经常在你的应用程序中使用这个,也许你想为API定义一个更小的结构体,它可以自动选择特定的字段。
type User struct {  ID     uint  Name   string  Age    int  Gender string  // hundreds of fields}type APIUser struct {  ID   uint  Name string}// 查询时自动选择“id”、“name”,SELECT `id`, `name` FROM `users` LIMIT 10db.Model(&User{}).Limit(10).Find(&APIUser{})
锁:GORM支持不同类型的锁。
// SELECT * FROM `users` FOR UPDATEdb.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)// SELECT * FROM `users` FOR SHARE OF `users`db.Clauses(clause.Locking{  Strength: "SHARE",  Table: clause.Table{Name: clause.CurrentTable},}).Find(&users)// SELECT * FROM `users` FOR UPDATE NOWAITdb.Clauses(clause.Locking{  Strength: "UPDATE",  Options: "NOWAIT",}).Find(&users)
SubQuery:子查询可以嵌套在查询中,使用* GORM . db对象作为参数时,GORM可以生成子查询。
// SELECT * FROM "orders" WHERE amount > (SELECT AVG(amount) FROM "orders");db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)// SELECT AVG(age) as avgage FROM `users` GROUP BY `name` HAVING AVG(age) > (SELECT AVG(age) FROM `users` WHERE name LIKE "name%")subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)
COUNT:获取匹配记录计数。
// SELECT count(1) FROM users WHERE name = "jinzhu"; (count)db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
修改保存所有字段
db.First(&user)user.Name = "jinzhu 2"user.Age = 100db.Save(&user)
更新字段
db.First(&user, "age = ?", 25)db.Model(&user).Updates(User{Name: "lisinew1", Age: 44})db.Model(&user).Updates(map[string]interface{}{"Name": "lisinew2", "Age": 35})db.Model(&user).Update("age", 30)
删除

如果您的模型包含一个gorm。DeletedAt字段(包含在gorm.Model中),它将自动获得软删除能力!

// GORM允许使用带有内联条件的主键删除对象db.Delete(&user, 1)// 假如Email"s ID is `10`,DELETE from emails where id = 10;db.Delete(&email)// 带附加条件删除db.Where("name = ?", "jinzhu").Delete(&email)// 永久删除db.Unscoped().Delete(&order)
原始SQL使用Scan查询Raw SQL
type Result struct {  ID   int  Name string  Age  int}var result Resultdb.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)var age intdb.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)var users []Userdb.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)
执行原始sql
// 执行删除表数据db.Exec("DROP TABLE users")// 执行带有表达式的更新db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")
DryRun模式
// Session Configurationtype Session struct {  DryRun                   bool  PrepareStmt              bool  NewDB                    bool  Initialized              bool  SkipHooks                bool  SkipDefaultTransaction   bool  DisableNestedTransaction bool  AllowGlobalUpdate        bool  FullSaveAssociations     bool  QueryFields              bool  Context                  context.Context  Logger                   logger.Interface  NowFunc                  func() time.Time  CreateBatchSize          int}// session modestmt := db.Session(&Session{DryRun: true}).First(&user, 1).Statementprintln(stmt.SQL.String()) println(stmt.Vars)
Row & Rows
// rowrow := db.Table("users").Where("name = ?", "jinzhu").Select("name", "age").Row()row.Scan(&name, &age)row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()row.Scan(&name, &age, &email)// rowsrows, err := db.Model(&User{}).Where("name = ?", "jinzhu").Select("name, age, email").Rows()defer rows.Close()for rows.Next() {  rows.Scan(&name, &age, &email)  // do something}rows, err := db.Raw("select name, age, email from users where name = ?", "jinzhu").Rows()defer rows.Close()for rows.Next() {  rows.Scan(&name, &age, &email)  // do something}
事务

GORM在事务内部执行写(创建/更新/删除)操作以确保数据一致性,如果不需要,可以在初始化时禁用它,之后将获得大约30%以上的性能提升。流程如下:

func CreateAnimals(db *gorm.DB) error {  // 注意,在事务中使用tx作为数据库句柄  tx := db.Begin()  defer func() {    if r := recover(); r != nil {      tx.Rollback()    }  }()  if err := tx.Error; err != nil {    return err  }  if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {     tx.Rollback()     return err  }  if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {     tx.Rollback()     return err  }  return tx.Commit().Error}
转换ID为主键:默认情况下,GORM使用名称为ID的字段作为表的主键。可以通过实现Tabler接口来更改默认表名,例如:
// 可以使用标记primaryKey将其他字段设置为主键type Animal struct {  ID     int64  UUID   string `gorm:"primaryKey"`  Name   string  Age    int64}
复数表名:GORM将结构名复数化为snake_cases作为表名,对于结构User,它的表名按照约定是users
// 转换表名为my_usersfunc (User) TableName() string {return "my_users"}
GORM允许用户通过覆盖默认的NamingStrategy来更改默认的命名约定,该策略用于构建TableName, ColumnName, JoinTableName, RelationshipFKName, CheckerName, IndexName
type Animal struct {  AnimalID int64     `gorm:"column:beast_id"`         // 将列名设置为 `beast_id`  Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设置为 `day_of_the_beast`  Age      int64     `gorm:"column:age_of_the_beast"` // 将列名设置为 `age_of_the_beast`}
分片

Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。对开发者友好、透明,使用上与普通 SQL、Gorm 查询无差别,只需要额外注意一下分表键条件。 为您提供高性能的数据库访问。

功能特点

非侵入式设计, 加载插件,指定配置,既可实现分表。轻快, 非基于网络层的中间件,像 Go 一样快支持多种数据库。 PostgreSQL 已通过测试,MySQL 和 SQLite 也在路上。多种主键生成方式支持(Snowflake, PostgreSQL Sequence, 以及自定义支持)Snowflake 支持从主键中确定分表键。

配置分片中间件,注册想要分片的表

import (  "fmt"  "gorm.io/driver/postgres"  "gorm.io/gorm"  "gorm.io/sharding")dsn := "postgres://localhost:5432/sharding-db?sslmode=disable"db, err := gorm.Open(postgres.New(postgres.Config{DSN: dsn}))db.Use(sharding.Register(sharding.Config{    ShardingKey:         "user_id",    NumberOfShards:      64,    PrimaryKeyGenerator: sharding.PKSnowflake,}, "orders").Register(sharding.Config{    ShardingKey:         "user_id",    NumberOfShards:      256,    PrimaryKeyGenerator: sharding.PKSnowflake,    // 对于show up give notifications, audit_logs表使用相同的分片规则。}, Notification{}, AuditLog{}))
序列化

序列化器是一个可扩展的接口,允许自定义如何使用databasae序列化和反序列化数据。

package mainimport ("database/sql/driver""encoding/json""fmt""gorm.io/driver/mysql""gorm.io/gorm")type Profile struct {Email  string `json:"email"`Mobile string `json:"mobile"`}type User struct {gorm.ModelName    string  `json:"name"`Age     int8    `json:"age"`Profile Profile `json:"profile" gorm:"type:json;comment:"个人信息""`}// 转换表名func (User) TableName() string {return "new_users"}// Value 存储数据的时候转换为字符串func (t Profile) Value() (driver.Value, error) {return json.Marshal(t)}// Scan 读取数据的时候转换为jsonfunc (t *Profile) Scan(value interface{}) error {return json.Unmarshal(value.([]byte), &t)}func main() {dsn := "root:123456@tcp(mysqlserver8:3306)/test?charset=utf8&parseTime=True&loc=Local"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {fmt.Println("data error")}db.AutoMigrate(&User{})user := User{Name: "刘海",Age:  23,Profile: Profile{Email:  "test1@qq.com",Mobile: "18822334455",},}db.Create(&user)var user1 Userdb.Debug().Where("profile->"$.mobile"=(?)", "18822334455").First(&user1)fmt.Println(user1)}

查看mysql已有新创建的new_users数据库和对应的数据

本人博客网站IT小神www.itxiaoshen.com

关键词:

推荐阅读

猜你喜欢

x 广告

Copyright   2015-2022 华南中介网版权所有  备案号: 粤ICP备18025786号-52   联系邮箱: 954 29 18 82 @qq.com