[Note] GORM 筆記
TL;DR
- query @ gorm
- method chaining @ form
db.
Table("table_name"). // Model(&model{})
Select("select syntax").
Group("field_names").
Order("field_name").
Find(&model{}) // Rows()
- methods 可以分成 chain methods、finisher methods、new session method。
與 Database 連線
- Connecting to a Database @ gorm
除了下述說明外,建議可以參考 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 中可以透過 struct
或 map
一次更新多個欄位,但要特別留意的是
- 使用 struct 的方式只會更新 non-zero value,也就是說如果值是
false
或0
這類的都不會被保存 - 使用
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
,舉例來說,想要同時根據 name
和 sn
來做 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
}
需要兩個欄位同時都一樣時才會執行 Upsert,只有一個一樣不會 Upsert。
如果希望只 upsert 有提供的欄位
使用者種方式時,要留意 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- 如果在一個 model 中設定多個
primaryKey
則會是 Composite Primary Key。
- 如果在一個 model 中設定多個
- 以複數的
snake_cases
當作 table name,例如product_tags
- 以
snake_case
當作欄位的名稱,例如is_publish
- 使用
CreatedAt
和UpdatedAt
來追蹤新增和修改的時間
覆蓋掉預設的資料表名稱
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 中後,會變成 AuthorName
和 AuthorEmail
:
// 程式來源: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 中有
UserID
和PatientID
- 還有新添加欄位
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
這些欄位:
- 在建立 Patient 時,一併把和 User 的關聯建立起來(也就是新增一筆記錄在 users_patients 資料表中)
- 在建立 Patient 前先使用
db.SetupJoinTable(model interface{}, field string, joinTable interface{})
的方法
// ./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",
})
這裡使用 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 建立關聯
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(Measurement
和 Devices
),並且取得 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 中的 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
預設的情況下,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(利如,0
或 00000-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
的值:
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,
}