GORM Guides 따라가기 1편

didnlie23·2023년 4월 4일
0

golang

목록 보기
3/3
post-thumbnail

Reference

Quick Start

일단 설치하고 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의 경우 기본적으로 DATEDATETIME 타입의 값을 []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)

Declaring Models

모델은 아래의 코드와 같이 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
}

또한 ScannerValuer 인터페이스를 구현한 커스텀 타입들로도 구성할 수 있다고 한다.
실제로 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
}

Conventions

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
}

Advanced

Field-Level Permission

기본적으로 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으로 포함되지 않는다고 한다.

Creating/Updating Time/Unix (Milli/Nano) Seconds Tracking

앞서 설명했다시피 GORM은 기본적으로 삽입, 수정 시각을 추적할 때CreatedAtUpdatedAt 필드를 사용하는데, 이때 자동으로 작업 수행 시각을 갱신해준다. 실제로 Quick Start 코드의 실행 결과를 통해 갱신된다는 것을 알 수 있었다.
만일 이를 커스텀하고 싶다면 GORM Config의 NowFunc 필드의 값(함수)를 수정하면 된다고 한다.

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

Embedded Struct

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
}

Fields Tags

이 밖에도 모델(구조체)를 정의할 때, 다양한 태그들을 추가할 수 있다고 한다.
https://gorm.io/docs/models.html#Fields-Tags

primaryKey 태그의 경우 autoIncrement가 내장되어 있는지에 대한 여부가 궁금했는데, 여기에서 내장되어 있다는 사실을 알 수 있었다. 또한 constraints를 추가할 수 있는 check, default value를 설정할 수 있는 default 태그가 있는 것을 확인했다.

profile
with golang

0개의 댓글