일단 설치하고 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
태그가 있는 것을 확인했다.