跳至主要内容

[Note] GORM 筆記

TL;DR

db.
Table("table_name"). // Model(&model{})
Select("select syntax").
Group("field_names").
Order("field_name").
Find(&model{}) // Rows()
  • methods 可以分成 chain methodsfinisher methodsnew session method

與 Database 連線

除了下述說明外,建議可以參考 go-snippets/pkg/gorm

import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func initDatabase(dsn string) (*database.GormDatabase, error) {
db, err := database.New(dsn, &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})

if err != nil {
return nil, err
}

if err := db.AutoMigrate(new(model.Organization)); err != nil {
log.Fatal(err.Error())
}

fmt.Println("Database connected ...")

return db, nil
}

func main() {
dsn := "sslmode=disable host=localhost port=5432 user=postgres dbname=gorm_sandbox"
db, err := initDatabase(dsn)
}

CRUD Interface

template/database/product @ go-snippets

Update

Update @ gorm

db.Update()

在 Update 中可以透過 structmap 一次更新多個欄位,但要特別留意的是

  • 使用 struct 的方式只會更新 non-zero value,也就是說如果值是 false0 這類的都不會被保存
  • 使用 map 的方式才會更新 zero-value,因此個人會建議使用 map 的方式來避免可能的 bug
// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

💡 會建議使用 map 的方式(而非 struct)來進行更新避免可能的 bug

db.Save()

使用 db.Save() 時,沒給的值會全部被清空(包括 createdAt、除了 updatedAt),所以如果要用 save,一定要把舊有的資料先拉出來,對要改變的內容進行修改後,在呼叫 save

// https://gorm.io/docs/update.html
// 先把原有資料拉出來
db.First(&user)

// 修改要改動的欄位就好
user.Name = "jinzhu 2"
user.Age = 100

// 使用 Save
db.Save(&user)

⚠️ 使用 db.Save() 時所有的欄位都會更新,因此若沒給的話會導致該值變回 zero-value,記得一定要先把資料拉出來後,修改要改的資料後,在呼叫 Save。

Upsert

Upsert-On-Conflict @ Create > Upsert

  • 使用 Gorm 內建的 upsert 方法會類似 Save,有指定更新欄位的一定要給值,否則會被清空
  • 使用 Where + Assign + FirstOrCreate` 組出來的 upsert 方法會類似 update,需要留意當使用者想要清空原本的資料時,因為後端接收到的是 zero value,所以不會執行清空的動作

根據單一欄位

upsert 的欄位需要是 unique 的才能,假設 upsert 時要根據的欄位只有一個,只需要將該欄位改成 unique 後即可:

type Product struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
Price uint
SN uint `gorm:"unique"`
Comment string
}

接著就可以根據這個欄位來決定要不要 Upsert:

⚠️ 如果希望被刪除的資料事後 Upsert 時可以復原,則在 Columns 中要加入 deleted_at 。如果希望 updated_at 在 upsert 時會一併更新,也要加在 Columns 中。

func (d *GormDatabase) UpsertProduct(product *model.Product) error {
if err := d.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sn"}}, // 根據 sn 這個欄位判斷要不要 upsert
DoUpdates: clause.AssignmentColumns([]string{
"price", "comment", "name", "updated_at",
}), // 要更新的欄位
}).Create(&product).Error; err != nil {
return err
}
return nil
}

⚠️ 在 clause.AssignmentColumns() 有列出的欄位名稱都會被儲存,因此若沒給值的話,原本的值會被清空。

也可以直接使用 UpdateAll: true

// Update all columns expects "primary keys" to new value on conflict
db.Clauses(clause.OnConflict{
UpdateAll: true
}).Create(&users)

根據多個欄位

若 upsert 時要根據的欄位不只一個時,需要將多個欄位建立出一個 unique index,舉例來說,想要同時根據 namesn 來做 upsert 的方法。

model 的部分,會把 Name 和 SN 又定義相同的 uniqueIndex(即,gorm:"uniqueIndex:name_sn"):

type Product struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `gorm:"uniqueIndex:name_sn"`
Price uint
SN uint `gorm:"uniqueIndex:name_sn"`
Comment string
}

接著就可以同時根據這兩個欄位來做 upsert:

func (d *GormDatabase) UpsertProduct(product *model.Product) error {
if err := d.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "sn"}, {Name: "name"}}, // 同時根據兩個欄位
DoUpdates: clause.AssignmentColumns([]string{"price", "comment"}), // 要更新的欄位
}).Create(&product).Error; err != nil {
return err
}
return nil
}
warning

需要兩個欄位同時都一樣時才會執行 Upsert,只有一個一樣不會 Upsert。

如果希望只 upsert 有提供的欄位

Where + Assign + FirstOrCreate @ Gorm

warning

使用者種方式時,要留意 zero value 的情況,也就是如果使用者想要把該欄位清空時,因為清空時該欄位值會是 zero value,可能會更新不到

// UpsertDevice returns the client for the given id or nil.
func (d *GormDatabase) UpsertDevice(deviceInfo *model.Device) error {
device := model.Device{}

if err := d.DB.
Where("box_device_id = ? AND nis_patient_id = ?", deviceInfo.BoxDeviceID, deviceInfo.NISPatientID).
Assign(deviceInfo).
FirstOrCreate(&device).Error; err != nil {
return err
}

if err := d.DB.Take(deviceInfo, device.ID).Error; err != nil {
return err
}

return nil
}

Model

慣例(conventions)

在 GORM 中,慣例上會:

  • ID 當作 PK
  • 以複數的 snake_cases 當作 table name,例如 product_tags
  • snake_case 當作欄位的名稱,例如 is_publish
  • 使用 CreatedAtUpdatedAt 來追蹤新增和修改的時間

覆蓋掉預設的資料表名稱

keywords: Tabler, TableName

透過實作 Tabler interface 中的 TableName() string 方法,可以修改 GORM 在 auto migrate 時建立的 Table 名稱。例如下述程式碼最終會在 DB 中建立名為 blog_tag 的資料表:

type Tag struct {
// ...
}

// 實作 Tabler interface,最終會在 DB 中建立名為 `blog_tag` 的資料表
func (t Tag) TableName() string {
return "blog_tag"
}

透過 TableName() string 可以覆蓋預設的資料表名稱,但若是希望動態產生不同的資料表名稱則需使用 db.Scopes() 來達到,參考這裡NamingStrategy

如果想要修改預設的欄位名稱,可以在定義 struct 時使用 gorm:"column_name:foo_bar" 或使用 NamingStrategy

gorm.Model

GORM 在定義 Model 時,可以使用 gorm.Model,這預設會帶入 這些欄位到該 Model 中:

  • ID,
  • CreatedAt
  • UpdateAt
  • DeleteAt
type User struct {
gorm.Model
Author *string `json:"author"` // 字串是 "",數值是 0
Title *string `json:"name"` // 使用 pointer 的話,存到 database 的預設值會是 null,否則會是 zero value
Rating int `json:"rating"` // 注意這裡 rating 沒有用雙引號
}

如果想要自己定義接收 JSON 中屬性的名稱,記得要加上雙引號

  • 接收 JSON 屬性為 "author",存到 database 後,該欄位名稱也是 author,從 database 取出時亦會以小寫開頭
  • 接收 JSON 屬性為 "name",存到 database 後,該欄位名稱是 title,從 database 取出時亦會以小寫開頭
  • 這裡 rating 沒有用雙引號,從 database 取出時會以「大寫」開頭,即 Rating

Embedded Struct

如果需要在 Model 中直接嵌入(embedded)另一個 struct,而並非透過「參照」的方式指稱到另一個 model,可以使用 embedded

anonymous fields

// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type User struct {
gorm.Model
Name string
}
// equals
type User struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string
}

嵌入自己定義的 Struct

keywords: gorm:"embedded"

如果是想要 embedded 其他 struct,而不是要當成 has_one 或 belongs_to 這種關聯式 Model 使用的話,也可以加上 gorm:"embedded" 使用:

也就是說,可以直接透過 Blog.Name 取得作者的名稱,而不是 Blog.Author.Name

// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type Author struct {
Name string
Email string
}

type Blog struct {
ID int
Author Author `gorm:"embedded"`
Upvotes int32
}

// equals
type Blog struct {
ID int64
Name string
Email string
Upvotes int32
}

在嵌入的欄位加上前綴

keywords: embeddedPrefix

若有需要在嵌入時為每個 struct 加上前綴的話,則可以加上 gorm:"embedded;embeddedPrefix:foo_" 的用法:

如此要原本 Author 欄位嵌入到 Blog 中後,會變成 AuthorNameAuthorEmail

// 程式來源:https://gorm.io/docs/models.html#Embedded-Struct
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
Upvotes int32
}

// equals
type Blog struct {
ID int64
AuthorName string
AuthorEmail string
Upvotes int32
}

Query

Query @ GORM

Find, First, Take

var user *model.User
var users []*model.User

db.Find(&user) // 列出所有 Users
db.Take(&user, 1) // 找出 PK 為 1 的 user

db.First(&user) // 列出第一個 User
db.First(&user, 1) // 找出 PK 為 1 且排序為第一個的 user

Where

db.Where("name = ?", "aaronchen").Take(&user)  // 如果確定只會有一個人
db.Where("name = ?", "aaronchen").First(&user) // 從符合條件中以 PK 排序後取出第一個

Count

func (d *GormDatabase) CountProductByCondition(condition ...interface{}) (int64, error) {
var count int64

tx := d.DB.Model(&model.Product{})

if len(condition) == 1 {
tx.Where(condition[0])
} else if len(condition) > 1 {
tx.Where(condition[0], condition[1:]...)
}

err := tx.Count(&count).Error
return count, err
}

Many to Many

Many to Many @ form

Customize Join Table

在 gorm 中也可以自訂 join table,例如 user has and belongs to patients, patients has and belongs to user,這時候的 model 可以這樣定:

  • 在 User 中有 Patients 欄位
  • 在 Patient 中有 Users 欄位
  • 使用 many2many:join_table_name; 可以自訂 Join Table 的名稱
// model/user.go
type User struct {
ID uint `gorm:"primaryKey;uniqueIndex;autoIncrement"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
Username string `gorm:"type:varchar(20);uniqueIndex"`
Password string
Patients []Patient `gorm:"many2many:users_patients;"`
}

// model/patient.go
type Patient struct {
ID uint `gorm:"primaryKey;uniqueIndex;autoIncrement" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `json:"-"`
Name string `json:"name"`
Phone *string `gorm:"type:varchar(20)" json:"phone" binding:"required"`
Users []User `gorm:"many2many:users_patients;"`
}

對於客製化的 Join Table(UsersPatients) 可以是:

  • Join Table 中有 UserIDPatientID
  • 還有新添加欄位 Relationship
// ./model/users_patients.go
type UsersPatients struct {
ID uint `gorm:"primaryKey;uniqueIndex;autoIncrement" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
DeletedAt gorm.DeletedAt `json:"-"`
UserID uint `json:"userId"`
PatientID uint `json:"patientId"`
Relationship string `json:"relationship"`
}

在使用 Custom Join Table 的時候可以使用 SetupJoinTable 方法,如此 gorm 會幫忙處理 CreatedAt, UpdatedAt 這些欄位:

// ./database/patient.go


func (d *GormDatabase) CreatePatient(patient *model.Patient) error {
if err := d.DB.SetupJoinTable(&model.Patient{}, "Users", &model.UsersPatients{}); err != nil {
return err
}

if err := d.DB.Create(patient).Error; err != nil {
return err
}

return nil
}

Associations

Associations 和 Preload 的差別

  • 如果是要拿某一 Model 關聯到的另一個 Model 的資料,使用 Association
    • User has many Products,使用 Association 可以把此 User 擁有的所有 Products 撈出來回傳-回傳的是 Products
    • Association 操作的就是 Join Table
  • 如果是要拿某一個 Model,但希望把相關連的 Model 的資料嵌入在原本的 Model 中,使用 Preload
    • User has many Products,使用 Preload 可以把此 User 擁有的所有 Products 撈出來放到 User struct 中,回傳的會是帶有 Product 資料的 User -回傳的是 User(帶有 Product 資料在內)

Find Associations

  • 假設 Product has many Categories
  • 使用 association 最終會得到和此 Product 關聯的所有「Categories
  • 在 model.Product 中要記得放入 PK(productID
  • 找到 Association 後使用 Find 找出所有和該 Product 關聯的所有 Categories
// database
// 使用 association 最終會得到和此 Product 關聯的所有「Categories」
func (d *GormDatabase) FindAssociationCategories(productID uuid.UUID) ([]*model.Category, error) {
var categories []*model.Category

// model.Product 中要記得帶入 pk
associations := d.DB.Model(&model.Product{ID: productID}).Association("Categories")
if err := associations.Find(&categories); err != nil {
return nil, err
}

return categories, nil
}

// API:要帶入 ProductID(PK)
categories, err := s.db.FindAssociationCategories(product.ID)

可以搭配 where condition 使用,但要注意model.Product 中仍然要放入 PK(aka, productID)因為這是 JOIN 時會用到的 ID,只在 where 中使用 productId 將找不到資料:

// database
// 找出和此 Product 關聯的所有 Categories,且此 Categories 的條件可以透過 where 加以限制
func (d *GormDatabase) FindAssociationCategoriesWithConditions(ID uuid.UUID, conditions ...interface{}) ([]*model.Category, error) {
var categories []*model.Category

// model.Product 中要記得帶入 pk
tx := d.DB.Model(&model.Product{ID: ID})

// 這裡的 Where 會套用到 Category 身上
if len(conditions) == 1 {
tx.Where(conditions[0])
} else if len(conditions) > 1 {
tx.Where(conditions[0], conditions[1:]...)
}

if err := tx.Association("Categories").Find(&categories); err != nil {
return nil, err
}

return categories, nil
}

// API:需要帶入 ProductID 和希望搜尋的 Category 條件
categories, err := s.db.FindAssociationCategoriesWithConditions(product.ID, model.Category{
Name: "Category 4",
})
warning

這裡使用 where 的條件會套用到 categories

Append Associations

  • 假設 Product has many Categories
  • Append Associations 指的是把新的 Categories 建立後並與 Product 建立關聯
  • db.Model(&M) 中的參數 model 記得要帶入的是 pointer
// database
// 新增的 category 都會和帶入的 Product ID 建立關聯
func (d *GormDatabase) AppendAssociationCategory(ProductID uuid.UUID, category *model.Category) error {
return d.DB.Model(&model.Product{ID: ProductID}).Association("Categories").Append(category)
}

func (d *GormDatabase) AppendAssociationCategories(ProductID uuid.UUID, categories []*model.Category) error {
return d.DB.Model(&model.Product{ID: ProductID}).Association("Categories").Append(categories)
}

// API
err := s.db.AppendAssociationCategories(product.ID, []*model.Category{
{Name: "Category 5"},
{Name: "Category 6"},
})

Replace Associations

  • 假設 Product has many Categories
  • replace association = clear associations + append new associations
  • replace association 則會把所有原本和 Product 相關連的 Associations 移除(reference),建立新的 categories 後,把新的 categories 和提供的 Product ID 建立關聯
warning
  • replace 時會把所有原本和 Product 相關連的 「Associations (reference)」 移除,並不會移除 categories table 中已經被建立的資料(object)。
  • 如果是 has and belongs to many 的情況,會移除的是也就是 Join Table product_categories 中的資料
  • 如果是 User has many Products, Products belongs to User 的情況,會移除的是 Product Table 中對應的 userID,也就是移除關聯但不會把舊有的 product 資料刪除
// database
func (d *GormDatabase) ReplaceAssociationCategories(ProductID uuid.UUID, categories []*model.Category) error {
return d.DB.Model(&model.Product{ID: ProductID}).Association("Categories").Replace(categories)
}

// API
s.db.ReplaceAssociationCategories(product.ID, []*model.Category{
{Name: "Category 7"},
{Name: "Category 8"},
})

Delete Association

  • 假設 Product has many Categories
  • Delete Association 時,需要把要刪除的 categoryID 放入 categories slice 中
  • 只會刪除 Product 和 Categories 之間的「關聯(reference)」,不會把 Categories 的資料(object)給刪除
// database
func (d *GormDatabase) DeleteAssociationCategories(productID uuid.UUID, categories []*model.Category) error {
return d.DB.Model(&model.Product{ID: productID}).Association("Categories").Delete(categories)
}

// API:把要刪除的 category ID 放入 categories slice 中
s.db.DeleteAssociationCategories(product.ID, []*model.Category{
{ID: "3da18909-bde4-43b4-ab13-7a3fb7eceb37"},
{ID: "fe8db119-5944-45df-84ec-7371393051fb"},
})

Clear Association

  • 假設 Product has many Categories
  • 直接把所有與特定 ProductID 相關連的 categories reference 都刪除,不會刪除 table categories 本身的資料
// database
func (d *GormDatabase) ClearAssociationCategories(productID uuid.UUID) error {
return d.DB.Model(&model.Product{ID: productID}).Association("Categories").Clear()
}

// API
err := s.db.ClearAssociationCategories(product.ID)

Preload

  • 使用 db.Preload(clause.Associations) 可以撈出所有相關聯的欄位
// Model Struct
type User struct {
ID
Languages []Language
}

type Language struct {
ID
Name
}

回傳的會是 user:

// 會撈出第一個 user,而且該 User struct 中的 Languages 欄位會有所有資料
db.Preload("Languages").First(&user)

// User
//{
// id: 1,
// languages: [...],
//}

回傳 user_id 為 3 的 User,且包含 devices 資料在內:

d.DB.Preload("Devices").First(&User{ID: 3})

// User
//{
// id: 3,
// name: "foo",
// devices: [
// {
// id: 1,
// name: "device1"
// }, {
// id: 2,
// name: "device2"
// }
// ]
//}

Nested Model

同時 Preload 兩個 Model(MeasurementDevices),並且取得 nested Model 的資料(Devices.Detections):

d.DB.Preload("Measurement").Preload("Devices.Detections").Not("status", "withdraw").
Find(&users).
Order("id desc")

// Users
// [
// {
// id: 1
// measurement: {/* ... */}
// devices: [
// {
// id: 1
// detection: [
// {id: 1, event: "foo"}
// ]
// }
// ]
// }
//]

如果是更深層的 Model,例如 Visit belongs to Resident, Resident belongs to Floor, Bed, Unit, Room,想要撈出 Visit 裡的 Resident 的 Room 可以這樣寫:

var visits []*model.Visit
d.DB.Table("visits").Preload(clause.Associations).
Preload("Resident.Room").
Preload("Resident.Floor").
Preload("Resident.Bed").
Preload("Resident.Unit").
Find(&visits)

Query Nested Model

[Golang gorm - preload with deeply nested models] @ StackOverflow

假設 Patient has many Devices, Device has many Detections:

type Patient struct {
Measurement *Measurement `json:"measurement"`
Devices []Device `json:"devices"`
}

type Device struct {
Detections []Detection `json:"detections"`
}

type Detection struct {
DeviceID uint `json:"deviceId"`
PatientID uint `json:"patientId"`
}

如果想要取得的結構像這樣:

[
{
id: 3,
measurement: {
/* ... */
},
devices: [
{
id: 1,
patientId: 3,
detections: [
{
id: 8,
deviceId: 1,
patientId: 3,
},
],
},
],
},
// ...
];

可以使用的 GORM 語法為 Devices.Detections 即可同時撈出 Device 和 Detections 的資料,這裡因為一併拉取 Measurement 的資料,所以有另外使用 Preload("Measurement")

if err := d.DB.Preload("Measurement").
Preload("Devices.Detections").
Not("status", "withdraw").
Find(&patients).
Order("id desc").Error; err != nil {
return nil, err
}

常用功能

在欄位中儲存 JSON

在 postgreSQL 中可以保存 JSON 的資料作為值,如果使用的是 GORM 只需搭配 go-gorm/datatypes

Model

  • 在 model 中將欄位的型別定為 datatypes.JSON,database migration 之後,該欄位的型別將會變成 jsonb
package model

import "gorm.io/datatypes"

type Device struct {
// ...
Configuration datatypes.JSON `json:"configuration"`
}

Database

var devices []*model.Device

// 找出 configuration 物件中有 is_enabled 屬性的所有 devices 中
db.Find(&devices, datatypes.JSONQuery("configuration").HasKey("is_enabled"))

// 找出 configuration 物件中有 is_enabled 屬性的所有 devices 中
db.Find(foobar, datatypes.JSONQuery("configuration").Equals("foobar", "name"))

Raw SQL & SQL Builder

SQL Builder @ gorm

使用 db.Raw() 可以直接下 SQL

DB.
Raw(`SELECT patient_id,
date_trunc('day', TO_TIMESTAMP(recorded_at)) AS date,
SUM(count) as count
FROM activities
WHERE patient_id = ? AND recorded_at >= ? AND recorded_at <= ?
GROUP BY patient_id, date
ORDER BY date;`, patientID, begin, end).
Scan(&activities)

但除非是很特殊的 SQL,不然一般可以使用 ORM 提供的 API 即可完成,例如,和上面同樣的語法,改用 ORM 的寫法:

d.DB.Table("activities").
Select("patient_id, date_trunc('day', TO_TIMESTAMP(recorded_at)) AS date, SUM(count) as count").
Group("patient_id, date").
Find(&activities)

啟用 uuid 作為 Primary Key(PK)

在 PostgreSQL 啟動 uuid 作為 PK,需要先開啟 EXTENSION 的功能:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

也可以把上述指令透過 GORM 的 d.DB.Exec()執行:

func (d *GormDatabase) AutoMigrate() {
// enable format UUID as PK
d.DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";")

if err := d.DB.AutoMigrate(
&model.User{},
); err != nil {
log.Fatal(err.Error())
}
}

安裝 uuid 的 go package:

go get -u github.com/satori/go.uuid

修改 Model 中 ID 的 field tag,使用:

type Product struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Name string `json:"name"`
Price uint `json:"price"`
}

Example: golang gorm postgres uuid relation @ gist Extension exists but uuid_generate_v4 fails @ StackOverflow

避免時間儲存到秒數以下

gorm config

修改 Gorm Config 中的 NowFunc

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
NowFunc: func() time.Time {
return time.Now().Truncate(time.Second)
},
})

其他存入 database 的時間都先使用 Truncate(time.Second) 過濾:

// parse 外部傳來的時間(string)
occurredAt, err := time.Parse(time.RFC3339, deviceEvent.OccurredAt)
if err != nil {
return nil, fmt.Errorf("incorrect format of occurredAt: %v", deviceEvent.OccurredAt)
}

// 把 Truncate 後的時間存入 database
occurredAt.Truncate(time.Second),

不要限制 Foreign Key 的 Constraint

Foreign Key Constraint

預設的情況下,GORM 會對 Foreign Key 進行限制,也將是要建立某個 record 時,Foreign Key 一定要存在,否則無法建立。下面的錯誤訊息是因為在建立 device 時,沒有提供該 device 的 foreign key(即,space_id):

//insert or update on table "devices" violates foreign key constraint "fk_spaces_devices" (SQLSTATE 23503)"

讓 Model 的欄位接受 nil

之所以會有這個問題,是因為在定義 Device Model 的時候,並沒有允許該 SpaceID 為 nil,因此每次建立該筆 record 時,spaceID 會直接使用 zero value(利如,000000-00000),這時候因為在 Space 的資料表中找不到 ID 為 0 的 record,所以會噴錯。從下面可以看到,建立 device 的 sql 中,space_id 的值被設為了 00000-00...

INSERT INTO "device" ("created_at","updated_at","deleted_at","space_id") VALUES ('2021-02-05 17:54:21.57','2021-02-05 17:54:21.57',NULL,'00000000-0000-0000-0000-000000000000') RETURNING "id"

這時候只需要將 model 該欄位的型別改用 pointer,如此該欄位即可接受 nil 的值:

/model/space.go
type Device struct {
// ...
// belongs to Organization
SpaceID *uuid.UUID
Space *Space
}
信息

如果改為 pointer 後仍出現 foreign key 的錯誤,這時候可以看 GORM 實際下的 SQL 是不是有把 id 的部分帶入預設值導致的。

完全關閉限制(不建議)

如果希望關閉這個限制,可以在 config 中使用 DisableForeignKeyConstraintWhenMigrating

config.GormConfig = &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NowFunc: func() time.Time {
return time.Now().Truncate(time.Second)
},
DisableForeignKeyConstraintWhenMigrating: true,
}