일단 설치하고 Quick Start 코드를 실행해봤다. 컴퓨터에 MySQL 데이터베이스가 설치되어 있었기 때문에 조금의 수정이 필요했다.
sql: Scan error on column index 1, name "created_at": unsupported Scan, storing driver.Value type []uint8 into type *time.Time
https://github.com/go-sql-driver/mysql#timetime-support
원래 MySQL의 경우 기본적으로 DATE와 DATETIME 타입의 값을 []byte 타입으로 반환하지만, time.Time 타입으로 받기를 원한다면 DSN 파라미터로 parseTime=true을 추가하면 된다고 한다.
db, err := gorm.Open(mysql.Open("root:password@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
위의 코드로 데이터베이스와 연결하는 부분을 수정했고, 실행에 성공했다.
실행 후 확인해본 결과, 테이블, 하나의 레코드가 삽입되었고, 값이 수정된 후 삭제되었다.
레코드 자체가 지워지지 않았고, delete_at column의 값이 갱신되었다.
mysql> select * from products;
+----+-------------------------+-------------------------+-------------------------+------+-------+
| id | created_at | updated_at | deleted_at | code | price |
+----+-------------------------+-------------------------+-------------------------+------+-------+
| 1 | 2023-04-04 14:08:30.901 | 2023-04-04 14:08:30.908 | 2023-04-04 14:08:30.910 | F42 | 200 |
+----+-------------------------+-------------------------+-------------------------+------+-------+
1 row in set (0.00 sec)
모델은 아래의 코드와 같이 Go의 기본 타입들과 포인터, Alias 타입으로 구성된 구조체로 선언할 수 있는 것 같다.
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
}
또한 Scanner와 Valuer 인터페이스를 구현한 커스텀 타입들로도 구성할 수 있다고 한다.
실제로 sql.NullString 타입의 경우 위의 두 인터페이스를 모두 구현한 구조체이다.
type NullString struct {
String string
Valid bool // Valid is true if String is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullString) Scan(value any) error {
if value == nil {
ns.String, ns.Valid = "", false
return nil
}
ns.Valid = true
return convertAssign(&ns.String, value)
}
// Value implements the driver Valuer interface.
func (ns NullString) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return ns.String, nil
}
GORM은 설정보다 컨벤션을 우선시하기 때문에, 추가 설정을 거의 하지 않으면서 사용하고 싶다면 약속을 지켜달라고 한다.
ID를 primary key로 사용하고, 테이블의 이름과 column 이름들은 snake_case의 형태로 매핑된다고 한다. 또한 CreatedAt, UpdatedAt 필드를 생성과 수정 시각을 추적하는데 사용한다고 한다.
아래는 GORM에서 미리 정의해둔 구조체이고, 앞서 설명한 내용들이 포함되어 있다.
// gorm.Model definition
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
이 구조체를 개발자가 정의할 모델, 즉 구조체에 내장(embed)해서 사용하면 편할 것이고, 실제로 Quick Start 코드의 Product 구조체 또한 해당 구조체를 내장하고 있다.
type Product struct {
gorm.Model
Code string
Price uint
}
기본적으로 Exported 필드들, 즉 대문자로 시작하는 필드들은 GORM을 통한 CRUD 작업 허용 대상이 된다. 데이터베이스에서 레코드를 읽고, GORM을 통해 레코드를 쓰는 작업을 의미하는데, 필드 태그를 통해 허용 여부를 제어할 수 있다고 한다. (only-read, write-only, create-only, update-only, ignored)
type User struct {
Name string `gorm:"<-:create"` // allow read and create
Name string `gorm:"<-:update"` // allow read and update
Name string `gorm:"<-"` // allow read and write (create and update)
Name string `gorm:"<-:false"` // allow read, disable write permission
Name string `gorm:"->"` // readonly (disable write permission unless it configured)
Name string `gorm:"->;<-:create"` // allow read and create
Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
Name string `gorm:"-"` // ignore this field when write and read with struct
Name string `gorm:"-:all"` // ignore this field when write, read and migrate with struct
Name string `gorm:"-:migration"` // ignore this field when migrate with struct
}
참고로 ignored 필드의 경우 GORM Migrator에 의해 테이블을 생성할 때 column으로 포함되지 않는다고 한다.
앞서 설명했다시피 GORM은 기본적으로 삽입, 수정 시각을 추적할 때CreatedAt과 UpdatedAt 필드를 사용하는데, 이때 자동으로 작업 수행 시각을 갱신해준다. 실제로 Quick Start 코드의 실행 결과를 통해 갱신된다는 것을 알 수 있었다.
만일 이를 커스텀하고 싶다면 GORM Config의 NowFunc 필드의 값(함수)를 수정하면 된다고 한다.
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
NowFunc: func() time.Time {
return time.Now().Local()
},
})
GORM에서 미리 정의한 gorm.Model 구조체를 내장(embed)한 Product 구조체를 Quick Start 코드에서 먼저 볼 수 있었는데, 내장된 구조체의 필드들은 부모 구조체에 자동으로 포함된다고 한다.
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
}
내장이 아닌 일반 구조체 타입의 필드의 경우는 태그를 통해 내장할 수 있다고 한다.
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
}
또한 embeddedPrefix 태그를 정의하여 내장될 필드에 Prefix를 추가할 수 있다고 한다.
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
}
이 밖에도 모델(구조체)를 정의할 때, 다양한 태그들을 추가할 수 있다고 한다.
https://gorm.io/docs/models.html#Fields-Tags
primaryKey 태그의 경우 autoIncrement가 내장되어 있는지에 대한 여부가 궁금했는데, 여기에서 내장되어 있다는 사실을 알 수 있었다. 또한 constraints를 추가할 수 있는 check, default value를 설정할 수 있는 default 태그가 있는 것을 확인했다.