[Go] Ent 사용해보기

leeeeeoy·2021년 9월 24일
1

이 글은 공식 사이트와 블로그 자료를 참고하여 정리한 글입니다.

Ent란??

Facebook에서 개발한 Go진영의 ORM이다. 가장 큰 특징 중 하나는 Go 코드로 스키마를 작성하면 DB에 모델링이 된다는 점이다. 즉 Go 코드로 작성 후 코드를 자동으로 생성해준다는 특징이 있다. Graph 탐색에도 특화되어 있는데 이 글에서는 공식 문서를 참고하여 기본 사용법을 익혀봤다.

사실 Go를 사용해본 사람은 다 알겠지만 Go진영에서는 Java진영의 JPA와 같은 강력한 ORM은 없다. gorm, xrom 등 여러가지 ORM이 있지만 JPA와 같이 정답처럼 사용되지는 않는 것 같다. 오히려 각자 편한걸 알아서 선택해서 사용하는 느낌이다(흔히들 이런게 Go스럽다고 하는데 사실 난 잘 모르겠다). 그러던 중 ent를 알게 되었고 Go코드로 스키마를 정의하고, 관련 메서드를 생성해준다는 점에 혹해서 한 번 사용해봤다. 사실 요즘 코드 제너레이션에 흥미가 생겨서인지 뭔가 더 궁금하게 다가왔던 것 같다. 아무튼 사용해보면서 느낀 것들을 차근차근 정리해봤다.

사용 준비

ent 설치

go get entgo.io/ent/cmd/ent

go get 명령어를 이용해서 해당 패키지를 다운받아 준다.

사용할 스키마 생성

go run entgo.io/ent/cmd/ent init <Entity 이름>

설치 후 다음과 같은 명령어로 정의하고 싶은 Entity의 이름과 함께 입력한다. 그렇게 되면 다음과 같이 폴더가 생성된다.

project
  ├── ent
  │   ├── generate.go
  │   └── schema
  │       └── user.go
  ├── go.mod
  └── go.sum

ent 폴더 안에 schema 파일이 생성되는데, 여기에 사용할 Entity를 정의하면 된다. 처음 명령어를 실행하면 다음과 같이 생성되어 있을 것이다.

schema/user.go

package schema

import (
	"entgo.io/ent"
)

// User holds the schema definition for the User entity.
type User struct {
	ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
	return nil
}

// Edges of the User.
func (User) Edges() []ent.Edge {
	return nil
}

예시 코드의 경우 User로 생성해주었다.

스키마 정의

생성된 파일에 이제 사용하고 싶은 스키마를 정의하면 된다. 생성된 파일에서 볼 수 있듯이 Fields와 Edges가 있는데 각각 DB의 컬럼과 관계를 정의한다.

Fields

  • DB에서 테이블의 컬럼을 정의한다.
  • 컬럼을 생성하면서 몇가지 옵션을 정의가 가능하다
  • 기본적으로 ID 칼럼은 자동적으로 생성되기 때문에 따로 정의하지 않아도 된다.
// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
		field.String("email"),
	}
}

기본적인 필드 정의 방법은 다음과 같다. 선언할 필드의 타입을 선언하고 뒤에 컬럼명을 입력해주면 된다. 컬럼을 정의하는데 많은 옵션들이 있는데 자세한건 공식문서에 정리가 잘 되어있다(Facebook 정도나 되는 큰 기업이라 그런지 관리가 엄청 잘 되는 것 같다!). 이 글에서는 사용한 몇가지 기능만 간단하게 정리해봤다.

// Fields of the User.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").SchemaType(map[string]string{
			dialect.MySQL: "varchar(10)",
		}),
		field.String("email").SchemaType(map[string]string{
			dialect.MySQL: "varchar(20)",
		}),
		field.Time("create_at").Default(time.Now),
		field.Time("update_at").Default(time.Now).UpdateDefault(time.Now),
	}
}
  • SchemaType: DB마다 컬럼의 데이터 타입을 다르게 지정하고 싶을 때 사용한다. 예시 코드의 name 필드의 경우, MySQL에서 해당 필드의 타입을 varchar(10)으로 사용한다고 정의한 것이다(설정을 하지 않고 실행시킬 경우 varchar(255)으로 정의되었다.)
  • Default: 컬럼의 기본값을 지정할 수 있다.
  • UpdateDefault: 해당 컬럼을 업데이트 할 때 기본값을 설정한다.

이렇게 하면 사용할 User의 스키마를 정의한 것이다. Go 코드로 작성을 하다보니 상당히 직관적으로 작성할 수 있는 것 같아서 좋았다.

Edges

  • Entity 사이의 연관관계를 정의한다.
  • 이 글에서는 User(1)와 Diary(N)를 예시로 들었다.
// Fields of the Diary.
func (Diary) Fields() []ent.Field {
	return []ent.Field{
		field.String("emotion"),
		field.String("condition"),
		field.String("texts"),
		field.Int("user_id"),
		field.Time("create_at").Default(time.Now),
		field.Time("update_at").Default(time.Now).UpdateDefault(time.Now),
	}
}

다음과 같이 Diary 스키마 필드를 정의했다.

// user.go

// Edges of the User.
func (User) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("diaries", Diary.Type),
	}
}



// diary.go

// Edges of the Diary.
func (Diary) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("users", User.Type).
			Ref("diaries").
			Unique().
			Field("user_id").Required(),
	}
}

그 후 Edges를 다음과 같이 정의했다. Edges 사용 방법은 역시 공식문서에 정리가 잘 되어있다. 예시 코드에서는 User가 Diary를 관리하므로 Diary가 User로부터 참조하기 때문에 Diary 쪽에는 From, User 쪽에는 To를 이용해 정의한다.

추가내용

Golang으로 백엔드 개발하기 -1 https://umi0410.github.io/blog/golang/how-to-backend-in-go-db/ 참고

일반적으로 1:N 관계를 맺을 때, DB에서는 N쪽이 연관관계의 주인이 된다. 즉 N쪽에서 1의 참조하는 내용을 정의하는데 ent에서는 그 반대로 동작한다(사실 아직 참조관계에 대한 공부가 부족해 확 와닿지는 않았다...ㅠㅜ). 즉 DB에서는 N쪽이 연관관계의 주인이 되지만, ent에서는 1이 N을 관리한다는 의미에서 1쪽이 주인이 되는 셈이다. 이 부분은 오랫동안 JPA와 같은 ORM을 사용해오신 분들에게는 헷갈리는 내용인 것 같다. 개인적으로 아직 공부를 하는 입장에서 ent 방식의 연관관계 정의는 크게 낯설지가 않았다

스키마를 정의한 후 다음과 같이 명령어를 입력하면 정의한 스키마에 따라 필요한 코드가 생성된다.

go generate ./ent

사용

main.go

package main

'
'
'

func main() {
    client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("failed opening connection to sqlite: %v", err)
    }
    defer client.Close()
    
    if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }
    
 
    u, err := client.User.
        Create().
        SetName("leeeeeoy").
        SetEmail("naver.com")
        Save(context.TODO())
    if err != nil {
        return nil, fmt.Errorf("failed creating user: %w", err)
    }
    log.Println("user was created: ", u)
}

다음과 같이 사용할 함수들이 정의된다. 간단하게 sqlite를 이용해서 User를 생성해보는 예시 코드이다. 실행시켜보면 정의한 스키마에 따른 DB테이블과 User가 생성되는 것을 확인할 수 있다. 공식문서를 보면 기본적인 CRUD관련 설명이 잘 정리되어있다(사실 공식 문서가 워낙 친절해서 특별히 적을게 없을 정도인 것 같다...).

정리

간단하게 ent를 사용하는 법을 익혀봤다. 원래는 gorm이나 sqlx를 사용했는데 gorm은 어딘가 모르게 설명이 조금 부족하기도 하고 모델을 정의하는데 태그를 이용하는 방식이 그렇게 익숙하지는 않았다. sqlx는 많은 기능들이 있지만 ORM이 아니기도 하고, 쿼리를 직접 작성해야 했기에 불편한 부분들이 있었다. ent의 경우 스키마 및 연관관계를 Go코드로 정의하기 때문에 비교적 직관적으로 작성할 수 있는 것 같고, 아직 많이 사용해보진 않았지만 관련 메서드를 생성해주는 것 역시 장점이 많아보였다(연관관계에 따른 메서드들도 같이 정의해준다). 사실 ORM이기 때문에 어쩌면 당연할 수도 있는 것들이지만 gorm이나 xorm에 비해 비교적 사용하기 쉽고 공식 문서가 잘 정리되어 있다는 점이 끌렸던 것 같다. 앞으로 종종 사용해봐야겠다.

참고자료

profile
100년 후엔 풀스택

0개의 댓글