• 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{})
  • 相关阅读:
    (SQL)如何将exec执行结果放入外面的变量中--动态sql语句
    Building and Installing ACE on Win32 with MinGW/ MSYS
    TAO和CIAO 简介
    Papervision3D 基础
    ACE网络开发中,各种Reactor实现的总结
    [转]第四期FLASH专家交流会 7月24日 广州东方国际饭店
    ACE介绍及简单服务器实例
    ACE相关资源
    Windows下用Eclipse搭建C/C++开发环境
    ACE的编译和安装,for windows、Linux
  • 原文地址:https://www.cnblogs.com/zzmx0/p/16224593.html
Copyright © 2020-2023  润新知