Golang GORM实战(三) | 数据模型

1,681 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

文章首发于公众号【程序员读书】,欢迎关注。

这是《Golang GORM实战》系列的第三篇,在这篇文章中我们来聊一聊GORM数据模型的一些细节。

模型定义

GORM的数据模型是一个标准的Go struct,一个数据模型代表一张数据表,模型由基本数据类型和实现了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.Model

对于一张数据表的每一条数据来说,都有自增主键,创建、更新与删除时间等比较通用的字段,因此GORM将其抽出来并定义了gorm.Model,如:

// gorm.Model 的定义
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

如果有需要,我们可以将gorm.Model嵌入到我们的模型中,而不用重复定义那几个字段,如:

type User struct{
    gorm.Model
    Name string 
}

上面嵌入gorm.Model的User等同于下面的User:

type User struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
  Name      string 
}

嵌入结构体

上面我们已经在自己定义的结构体中嵌入gorm.Model,当然,我们也可以嵌入自定义的结构,匿名嵌入,这种方式与上面嵌入gorm.Model相同,如:

type User struct{
    Username string
    Age      int 
} 


type House struct{
    ID   int 
    Name string
    User
}

//等同于

type House struct{
    ID       int 
    Name     string
    Username string
    Age      int 
}

而非匿名嵌入,也就是正常的结构体字段,则需要声明字段标签embedded才可以嵌入,否则会被GORM当作关联模型处理,但于未定义关联关系,所以会报错,如:

//错误
type House struct{
    ID   int 
    Name string
    User User
}

//正确
type House struct{
    ID   int 
    Name string
    User User   `gorm:"embedded"`
}

另外,无论是正常结构体嵌入还是匿名嵌入,都可以使用字段标签embeddedPrefix来指定嵌入结构全部字段的前缀,如:

type House struct{
    ID   int 
    Name string
    User User   `gorm:"embedded;embeddedPrefix:house_"`
}

//等同于

type House struct{
    ID            int 
    Name          string
    HouseUsername string 
    HouserAge     int 
}

主键ID

GORM默认会把ID作为数据表的主键,如:

type User struct {
  ID   string // 默认情况下,名为 `ID` 的字段会作为表的主键
  Name string
}

如果ID是整型且为自增字段的话,则在新增时,会自动填充该值,如:

//ID此时为0
var u = &User{Name:"小张"}

//创建成功后,此时ID自动填充为数据表自增字段的值
db.Create(u)

如果想设置其他字段为主键的话,可以通过字段标签primaryKey来指定,如:

type User struct {
  UserID string `gorm:"primaryKey"`
  Name   string
}

另外,也可以将多个字段设置为字段,而多个字段组成的主键也叫复合主键,如:

type Student struct{
    StudentID uint64 `gorm:"primaryKey"`
    ClassId   uint64 `gorm:"primaryKey"`
}

无论是ID,还是通过primaryKey设置的单字段主键或联合主键,如果字段的数据类型为整型,GORM都会把该字段设置为自增字段(AUTO_INCREMENT),可以通过autoIncrement设置为false来禁用这个功能,如:

type Student struct{
    StudentID uint64 `gorm:"primaryKey;autoIncrement:false"`
    ClassId   uint64 `gorm:"primaryKey;autoIncrement:false"`
}

时间追踪

GORM约定使用CreatedAt,Updatedat,DeletedAt追踪记录创建,更新,软删除的时间,如果在数据模型中定义了这几个字段,则在创建、更新、软删除记录时,会自动填充时间。

CreatedAt

如果数据模型有CreatedAt字段时,在创建记录时,如果没有指CreatedAt的值,则会将字段的值设置为当前时间。

db.Create(&user) // 将 `CreatedAt` 设为当前时间

也可以自己指定

user2 := User{Name: "jinzhu", CreatedAt: time.Now()}
db.Create(&user2) // user2 的 `CreatedAt` 不会被修改

也可以手动修改

db.Model(&user).Update("CreatedAt", time.Now())

UpdatedAt

//Update方法与UpdateColumn的区别
db.Model(&user).Update("name", "jinzhu") // 会将 `UpdatedAt` 设为当前时间

db.Model(&user).UpdateColumn("name", "jinzhu") // `UpdatedAt` 不会被修改

//对于Create方法来说,指定UpdatedAt就不更新,未指定则更新
user2 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Create(&user2) // 创建记录时,user2 的 `UpdatedAt` 不会被修改

//对于Save方法来说,都更新
db.Save(&user) // 将 `UpdatedAt` 设为当前时间
user3 := User{Name: "jinzhu", UpdatedAt: time.Now()}
db.Save(&user3) // 更新时,user3 的 `UpdatedAt` 会修改为当前时间

DeletedAt

DeletedAt字段在使用Delete方法删除数据表记录会起作用,具体我们在系列的其他文章讲解到删除记录会涉及到。

覆盖表名

GORM使用结构体的蛇形命名作为数据表名称,如,对于User结构体来说,其表名为users,而GameUser结构的表名为game_users。

不过,如果不想按GORM的规则来指定数据表名,我们可以让数据模型实现Tabler接口,该接口的定义如下:

type Tabler interface {
    TableName() string
}

让User结构体实现Tabler接口,如:

//表名为user,而不是users
func (u *User)TableName()string(){
    return "user"
}

当然,除了定义模型的TableName方法外,也可以使用Scope动态生成表名,或者在执行GORM的Create, First, Find, Take, Save, Update, Delete等方法时,临时指定数据表名,当然如果你想指定模型的表名的话,定义模型的TableName方法是最简单,因此推荐通过这种方法来指定表名。

覆盖列名

数据表的列名使用的也是数据模型的蛇形命名,如:

type User struct{
    ID   int    //列名是id
    name string //列名是name
}

不过,可以使用column标签来覆盖列表,如:

type User struct{
    ID INT `gorm:"column:user_id"` //列名为user_id
}

标签(tag)

字段标签

对于数据模型(model)来说,字段标签并不是必须的,而是可选的,tag大小写不敏感,如primarykeyprimaryKey是一样的,不过还是推荐按下表列出的名称去使用。

标签名(tag)标签说明
column指定数据表列名
type列数据类型
size指定列的大小
primaryKey指定列为主键,默认ID为主键
unique指定列为唯一
default指定默认值
precision指定列的精度
scale指定列大小
not null指定列为 NOT NULL
autoIncrement指定列为自动增长
autoIncrementIncrement自动步长,控制连续记录之间的间隔
embedded嵌套字段
embeddedPrefix嵌入字段的列名前缀
autoCreateTime创建时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime创建/更新时追踪当前时间,对于 int 字段,它会追踪秒级时间戳,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex与 index 相同,但创建的是唯一索引
check创建检查约束,例如 check:age > 13,查看 约束 获取详情
<-设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
->设置字段读的权限,->:false 无读权限
-忽略此字段
comment创建表时字段的注释

上述的字段标签有很大一部部分是用于迁移数据表的,不过这里不推荐使用GORM的数据迁移功能,这是因为对数据表的创建、删除、修改都是比较重大的变更和高危操作,因此需要严格评估后再实施操作,而不应该写在程序代码里。

而对于上述的标签的说明,有个大概印象就好,如须使用,再查看文档即可。

关联标签

另外,下面列出来的字段标签与数据模型的关联有关,我们在之后的文章再行讲解。

标签名(tag)标签说明
foreignKey指定当前模型的列作为连接表的外键
references指定引用表的列名,其将被映射为连接表外键
polymorphic指定多态类型,比如模型名
polymorphicValue指定多态值、默认表名
many2many指定连接表表名
joinForeignKey指定连接表的外键列名,其将被映射到当前表
joinReferences指定连接表的外键列名,其将被映射到引用表
constraint关系约束,例如:OnUpdate、OnDelete

小结

对于GORM来说,数据模型其实是就是Go struct,对应一个数据库的数据表,GORM约定了许多数据模型到数据表的映射规则,比如表名与列名的蛇形复数命命规则,默认ID字段为主键等,但我们仍然可以通过对数据模型方法和标签的定义来改变这些约定,比如通过TableName方法来改变模型对应的数据表等,对GORM数据模型的理解,可以让我们更好地去使用GORM操作数据表。