gorm

转载请注明出处!!!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{})


  • 作者:合十
  • 发表时间:2022年5月5日 15:49
  • 更新时间:2022年8月18日 22:15
  • 所属分类:GO语言

Comments

该文章还未收到评论,点击下方评论框开始评论吧~