转载请注明出处!!!http://www.he10.top/article/detail/51
博主一直是写python的,面对GO语言真是爱恨交加,它虽能够弥补python运行中的性能缺陷,但也给你带来从动态语言到静态语言的种种不适。
本文主要讲使用gorm中遇到的各种问题以及解决问题的思路、理解。如果你是想更彻底的了解gorm你得需要看下官网文档。请移步:gorm中文文档
gorm不如python sqlalchemy或Django的ORM封装的那么彻底,使用中能接触到更直接的操作mysql的东西,有弊也有利。
一、定义模型类
gorm提供了默认的模型类gorm.Model,它里面有4个字段,分别是:ID、CreateAt、UpdateAt、DeleteAt;ID为自增主键(定义的模型类中如有ID字段,默认为自增主键),CreateAt、UpdateAt、DeleteAt分别对应创建时间、更新时间、删除时间(软删除),如果你在定义的模型类中使用了gorm.Model,那么在Create、Update、Delete时会分别给他们赋值,像python的orm了。四个字段如需单独使用,仅需在模型类中定义相同的名称和类型即可,例如想自动赋值创建时间,那么在模型中定义一个CreateAt字段,类型为time.Time即可。
但需要注意的是,go中的time是timestamp(时间戳)类型,非datetime(时间格式字符串)类型,所以当你使用gin时,通过gin.H传给前端的时间是带有时区的(因为gin.H会去调time.Time结构体的MarShalJSON方法),不符合日常需求,那就需要自定义时间字段类型来解决了。下面主讲自定义时间字段、枚举字段和外键,因为gorm定义字段可以通过type一一对应mysql字段,所以其他类型字段没什么好讲的了
1.1 自定义时间字段类型
自定义字段需要定义Scan和Value两个方法接口,分别对应 从数据库中取出数据后的对应操作 和 将数据写入数据库的对应操作。在写后端接口时,如通过json传数据给前端,还可以定义MarshalJSON方法接口,这样就可以在字段值转json时自动化了
本地定义LocalDateTime结构体,如下:
1 package utils 2 3 import ( 4 "database/sql/driver" 5 "errors" 6 "fmt" 7 "time" 8 ) 9 10 type LocalDateTime time.Time 11 12 func (t LocalDateTime) MarshalJSON() ([]byte, error) { 13 // 默认情况给前端的时间格式 %Y-%m-%d 14 tTime := time.Time(t) 15 tStr := tTime.Format("2006-01-02") 16 return []byte(fmt.Sprintf("\"%v\"", tStr)), nil 17 } 18 19 func (t LocalDateTime) Value() (driver.Value, error) { 20 // 这里定义将数据写入数据库的对应操作,也就是将自定义的LocalDateTime数据类型如何转化成数据库中字段的数据类型 21 tTime := time.Time(t) 22 return tTime.Format("2006-01-02 15:04:05.000000"), nil 23 } 24 25 func (t *LocalDateTime) Scan(v interface{}) error { 26 // 这里定义从数据库中取出数据后的对应操作,也就是从数据库中取出到的数据类型如何转化为LocalDateTime的数据类型 27 switch vt := v.(type) { 28 case []byte: 29 tTime, _ := time.Parse("2006-01-02 15:04:05.000000", string(vt)) 30 *t = LocalDateTime(tTime) 31 case string: 32 tTime, _ := time.Parse("2006-01-02 15:04:05.000000", vt) 33 *t = LocalDateTime(tTime) 34 default: 35 fmt.Printf("%#v\n", vt) 36 return errors.New("类型处理错误") 37 } 38 return nil 39 } 40 41 func (t LocalDateTime) ParseDateTime() string { 42 // 特殊情况给到前端时间格式 %Y-%m-%d %H:%M 43 tTime := time.Time(t) 44 45 return tTime.Format("2006-01-02 15:04") 46 }
模型类中使用
1 import "heshi-backend/utils" 2 3 type BaseModel struct { 4 ID uint `gorm:"type:int(11) auto_increment;not null;primaryKey;" json:"id"` 5 CreateTime utils.LocalDateTime `gorm:"type:datetime(6);not null;column:create_time" json:"create_time"` 6 UpdateTime utils.LocalDateTime `gorm:"type:datetime(6);not null;column:update_time" json:"update_time"` 7 IsDelete bool `gorm:"type:bool;not null;column:is_delete" json:"is_delete"` 8 }
值得一题:
Value方法需要定义LocalDateTime为值类型,因为Go底层是通过值类型调用方法的,传入若是指针类型则将拿不到指针类型的方法,从而报 sql: converting argument $1 type 错误。Value方法对应写数据(增改),也就是说发生增改操作是会来调用该方法。
Scan方法需要定义LocalDateTime为指针类型,因为在拿到数据库数据后赋值给LocatDateTime对象,需要为LocalDateTime对象的指针才能正确赋值。Scan方法对应读数据,也就是发生读取操作时回来调用该方法。
如果使用了自定义的时间数据类型,将不能自动赋值,请在必要时手动赋值。
1.2 枚举类型
枚举类型需要自定义一个基本数据类型(string、int64【网上看到用int插入数据时可能会报错,用int64不会,未实测】等),然后通过该数据类型定义枚举常量,让字段需为这个数据类型即可,如下:
1 type attentionLevel int64 2 3 const ( 4 ATTENTION_ONE attentionLevel = 1 5 ATTENTION_TWO attentionLevel = 2 6 ATTENTION_THREE attentionLevel = 3 7 ATTENTION_FOUR attentionLevel = 4 8 ) 9 10 type ArticleType struct { 11 BaseModel BaseModel `gorm:"embedded" json:"base_info"` 12 Name string `gorm:"type:varchar(24);not null;column:name" json:"name"` 13 Count uint `gorm:"type:int(11);not null;column:count" json:"count"` 14 Attention attentionLevel `gorm:"type:smallint(6);not null;column:attention" json:"attention"` 15 Level int `gorm:"type:smallint(6);not null;column:level" json:"level"` 16 }
1.3 外键
说实话,官网的外键和关联真的挺难理解的,导致我在这里卡了很长时间,这里讲下一对多情况,理解了后一对一、多对多也自然就理解了,现分析出一个可用的结果如下(未用reference):
定义外键:
1 type ArticleType struct { 2 BaseModel BaseModel `gorm:"embedded" json:"base_info"` 3 Name string `gorm:"type:varchar(24);not null;column:name" json:"name"` 4 Count uint `gorm:"type:int(11);not null;column:count" json:"count"` 5 Attention attentionLevel `gorm:"type:smallint(6);not null;column:attention" json:"attention"` 6 Level int `gorm:"type:smallint(6);not null;column:level" json:"level"` 7 // 自关联外键, 可以为null,可以通过该id找到关联的ArticleType 8 ParentNameId *uint `gorm:"type:int(11);column:parent_name_id" json:"parent_name_id"` 9 // 关联自己的那些ArticleType,通过自关联外键ParentNameId查找 10 ChildArticleTypes []ArticleType `gorm:"ForeignKey:ParentNameId" json:"child_article_types"` 11 // 关联自己的那些Article,通过关联外键TypeId查找 12 Articles []Article `gorm:"ForeignKey:TypeId" json:"articles"` 13 } 14 15 type Article struct { 16 BaseModel BaseModel `gorm:"embedded" json:"base_info"` 17 Title string `gorm:"type:varchar(128);not null;column:title" json:"title"` 18 Content string `gorm:"type:LONGTEXT;not null;column:content" json:"content"` 19 BackgroundImage string `gorm:"type:varchar(100);not null;column:background_image" json:"background_image"` 20 LikeAmount uint `gorm:"type:int(11);not null;column:like_amount" json:"like_amount"` 21 CollectAmount uint `gorm:"type:int(11);not null;column:collect_amount" json:"collect_amount"` 22 IsTop bool `gorm:"type:bool;not null;column:is_top" json:"is_top"` 23 Level attentionLevel `gorm:"type:smallint(6);not null;column:level" json:"level"` 24 // 关联ArticleType外键,可通过该id找到关联的ArticleType 25 TypeId *uint `gorm:"type:int(11);column:type_id" json:"type_id"` 26 PageViews uint `gorm:"type:int(11);not null;column:page_views" json:"page_views"` 27 }
理解一下:ArticleType与Article一对多,ArticleType为一类、Article为多类;ArticleType由于有两层type所以自关联。多类中定义外键,关联着自己的一类对象(学过mysql的都知道该外键在一类中需为主键);一类中定义切片,可以通过多类关联自己的外键值找到所有关联自己的多类对象(可以参考mysql的sql语句理解)并赋值给该切片,该切片不会在mysql中创建字段,属于orm封装层范畴,如不需要一次性查找出所有关联自己的多类,可不定义。
较为离谱的是,python的orm到这里外键就定义结束了,但gorm不是的,还需要变态的在建表时(建表见2.2)添加上外键:
1 // 对模型类Article添加外键type_id,绑定tb_article_types表的id字段,定义模型类对应的表名称可通过TableName方法定义,具体可看文档 2 DB.Model(&model.Article{}).AddForeignKey("type_id", "tb_article_types(id)", "CASCADE", "CASCADE") 3 // 对模型类ArticleType添加外键parent_name_id,绑定tb_article_types表的id字段 4 DB.Model(&model.ArticleType{}).AddForeignKey("parent_name_id", "tb_article_types(id)", "CASCADE", "CASCADE")
外键查找数据:
多类找一类没什么好说的,通过外键在一类表中查找即可,下面说下通过一类找多类:
1 var parentTypes []model.ArticleType 2 if err := common.DB.Select([]string{"id", "name"}).Where("parent_name_id is null").Find(&parentTypes).Error; err != nil { 3 response.Error(ctx, "select parentType error") 4 return 5 } 6 for _, parentType := range parentTypes { 7 // 语法: DB.Association("[一类中定义的多类切片名称]").Find(&一类对象.多类切片名称) 8 if err := common.DB.Model(&parentType).Association("ChildArticleTypes").Find(&parentType.ChildArticleTypes).Error; err != nil { 9 response.Error(ctx, "select childTypes error") 10 return 11 } 12 }
需要注意:当外键可为空时,请将外键数据类型设置为基本数据类型的指针类型,原因参照1.4默认值注意。当不设置外键时,gorm默认的会插入外键对应数据类型的零值,指针的零值为nil,正好对应数据库中的null。
1.4 默认值需注意
gorm中字段如果定义的为值类型,并且设置了默认值时,当你想要给该字段插入其对应数据类型的零值(int类型为0,字符串类型为“”,布尔类型为false等)时,gorm不会采用你传入的值,而是将设置的默认值插入。下面对应sql语句举例:
1 type UserDemo struct { 2 ID uint 3 Name string `gorm:"type:varchar(12);not null;column:name" json:"name"` 4 Age uint `gorm:"type:int(3);not null;column:age" json:"age"` 5 Hometown string `gorm:"type:varchar(20);default:火星-火星市;column:hometown" json:"hometown"` 6 } 7 8 func Create() { 9 var user = UserDemo{ 10 Name: "寒江过瘾", 11 Age: 18, 12 Hometown: "", 13 } 14 // 此时对应的sql语句将是: insert into tb_users ("name", "age", "hometown") values("寒江过瘾", 18, "火星-火星市"); 15 common.DB.Create(&user) 16 }
如果就想将用户家乡设置为"",解决方案: 使用Value\Scan接口方式或将Hometown的数据类型设置为string指针类型即可
接口方式:
1 type UserDemo struct { 2 ID uint 3 Name string `gorm:"type:varchar(12);not null;column:name" json:"name"` 4 Age uint `gorm:"type:int(3);not null;column:age" json:"age"` 5 // sql.NullString封装了Value和Scan方法 6 Hometown sql.NullString `gorm:"type:varchar(20);default:火星-火星市;column:hometown" json:"hometown"` 7 }
指针方式:
1 type UserDemo struct { 2 ID uint 3 Name string `gorm:"type:varchar(12);not null;column:name" json:"name"` 4 Age uint `gorm:"type:int(3);not null;column:age" json:"age"` 5 Hometown *string `gorm:"type:varchar(20);default:火星-火星市;column:hometown" json:"hometown"` 6 }
1.5 update更新提示无表名称
今天在更新数据时,遇到了明明传入了结构体指针,调用Updates还是提示无表名称的错误 Error 1103: Incorrect table name ''
背景是写一个更新用户信息接口,在接口之前用了验证token中间件,验证通过将user结构体对象写入上下文,接口函数中拿到user结构体对象并更新相应数据然后更新数据库,起初报错的代码如下:
1 var updateInfo = make(map[string]string) 2 updateInfo["name"] = userInfo.UserName 3 updateInfo["hometown"] = userInfo.Hometown 4 updateInfo["maxim"] = userInfo.Maxim 5 6 user, _ := ctx.Get("user") 7 if err := common.DB.Model(&user).Updates(updateInfo).Error; err != nil { 8 response.Error(ctx, "更新用户信息失败") 9 return 10 }
通过Debug发现sql语句中并未设置表名和where条件,就像是完全未使用这个user结构体对象,调试了许久,发现通过对user类型断言,并将断言后新的结构体对象传入Model函数,可以正确更新数据,如下:
1 var updateInfo = make(map[string]string) 2 updateInfo["name"] = userInfo.UserName 3 updateInfo["hometown"] = userInfo.Hometown 4 updateInfo["maxim"] = userInfo.Maxim 5 6 user, _ := ctx.Get("user") 7 // 通过类型断言,将user转成model.User结构体对象并赋值给modelUser 8 modelUser := user.(model.User) 9 if err := common.DB.Model(&modelUser).Updates(updateInfo).Error; err != nil { 10 response.Error(ctx, "更新用户信息失败") 11 return 12 }
二、连接数据库 & 表迁移
2.1 连接数据库
通过gorm.Open即可完成连接数据库操作,会返回DB操作对象,默认已经是数据库连接池了
1 DB, err = gorm.Open("mysql", "[username]:[password]@([host:port])/[database]?charset=[charset]&parseTime=true&loc=Local")
后面加parseTime和loc参数可解决时区不一致导致时间不对的问题。
2.2 表迁移
通过DB.AutoMigrate可以完成模型类到表的迁移操作,表不存在则直接创建表,表存在则进行字段对比并添加字段,表完全一致不进行操作。
1 DB.AutoMigrate(&model.ArticleType{}, &model.Article{})