https://entgo.io/

entgo 는 Facebook Open Source에서 관리하는 go언어 ORM라이브러리 입니다.

entgo를 만나기 전에는 xorm 을 사용했는데, xorm 보다 깔끔하고, 사용하기 편하다는 인상을 남겨주었습니다.

entgo 설치

todo프로젝트 폴더를 만듭니다.

mkdir todo
cd todo
go mod init todo

entgo 라이브러리를 설치합니다.

go get entgo.io/ent/cmd/ent

entgo 구성을 위해 다음 명령어를 입력합니다.

go run entgo.io/ent/cmd/ent init User

다음처럼 ent폴더가 생성됩니다.

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

todo/ent/schema/user.go

package schema

import "entgo.io/ent"

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

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

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

Fields() 함수로 테이블의 칼럼(필드) 를 정의합니다.
Edges() 함수로 테이블끼리 의 관계를 정의합니다.

필드 정의하기

entgo에서 지원하는 자료형 입니다.

go
숫자 자료형 ex) int, uint8, float64,...
bool
string
time.Time
[]byte
JSON
Enum
UUID
Other

테이블을 정의하기 위해 Fields() 함수를 다음처럼 작성합니다.

func (User) Fields() []ent.Field {
	return []ent.Field {
		field.String("name"),
		field.Int("age"),
		field.String("phone"),
	}
}

다음 테이블이 정의됩니다.

todo
colomn id name age phone
go type - string int string
sql type - text int text

id는 entgo에서 자동으로 생성되고, 테이블에 값을 추가할때마다 auto increase 됩니다. field이름은 기본적으로 snake_case 를 따라야 합니다.

필드 기본 값 - Default()

Default() 함수로 필드의 기본 값을 설정 합니다. age의 기본 값을 0으로 설정 했습니다.

func (User) Fields() []ent.Field {
	return []ent.Field {
		field.String("name"),
		field.Int("age").
		Default(0),
		field.String("phone"),
	}
}

검증자 - Validate()

값 입력 전 정규식으로 값이 올바른지 확인할 수 있습니다. Match() 함수로 정규식을 사용하거나, Validate() 함수로 검증자 함수를 넣어줄 수 있습니다.

func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			Match(regexp.MustCompile("[a-zA-Z_]+$")).
			Validate(func(s string) error {
				if strings.ToLower(s) == s {
					return errors.New("group name must begin with uppercase")
				}
				return nil
			}),
		field.Int("age").
			Default(0),
		field.String("phone"),
	}
}

빌트인 검증자

정수형
Positive() 양수만 허용
Negative() 음수만 허용
NonNegative() 음수 비허용
Min(i) i 초과 값만
Max(i) 미만 값만
Range(i, j) i ~ j 값만
string
MinLen(i) 최소 길이
MaxLen(i) 최대 길이
Match(regexp.Regexp) 정규식 검사
NotEmpty "" 비허용

필드 Null 허용 - Optional()

기본적으로 entgo가 생성한 필드는 null 을 불허 합니다. nullable필드를 만들기위해 Optional() 함수를 사용합니다.

func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			Optional(),
		field.Int("age").
			Default(0),
		field.String("phone"),
	}
}

Go Type Null 허용 - Nillable()

Optional() 과 같이 사용합니다. Optional() 처럼 Nullable한 필드를 생성하는 것은 같으나, 코드 제네레이션후 구조체에 nil값을 저장할 수 있는 형태로 만들어집니다.

func (User) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").
			Optional(),
		field.String("sub").
			Optional().Nillable(),
	}
}

코드 제네레이션 후 다음 구조체가 생성 됩니다.

type User struct {
	// Optional()
	Name string `json:"name,omitempty"`
	
    // Optional(), Nillable()
	Sub *string `json:"sub,omitempty"`
}

불변 - Immutable()

입력한 값이 변경되지 않길 원하면, Immutable() 를 사용하면 됩니다.

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.Time("created_at").
            Default(time.Now).
            Immutable(),
    }
}

유니크 - Unique()

입력한 값이 칼럼 내에서 중복되지 않는 고유한 값이길 원하면, Unique() 를 사용하면 됩니다.

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
        field.String("nickname").
            Unique(),
    }
}

스토리지 키 - StorageKey()

커스텀 스토리지키를 지정합니다. Gremlin 에서 커스텀 칼럼 이름을 매핑하는등에 사용합니다.

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            StorageKey("old_name"),
    }
}

Struct Tags

코드 제네레이션후 생성될 구조체에 태그를 지정할 때 사용합니다. 기본적으로 Json 태그는 entgo가 생성하니 따로 지정할 필요 없습니다.

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            StructTag(`hello:"how are you"`),
    }
}

민감한 필드 - Sensitive()

Sensitive()를 설정하면, 코드 제네레이션 후 구조체에 태그가 붙지 않습니다. 그리고 해당 필드는 출력되거나 직렬화되지 않습니다.

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("password").
            Sensitive(),
    }
}

코드 제네레이션

정의한 필드를 go 에서 사용하기위한 코드를 생성 합니다.

필드를 정의합니다.

func (User) Fields() []ent.Field {
	return []ent.Field {
		field.String("name").
			Unique(),
		field.String("sub_name").
			Optional(),
		field.Int("age").
			Positive(),
		field.String("password").
			Sensitive(),
	}
}

콘솔로 다음 명령어를 실행합니다.

go generate ./ent
go mod tidy
./ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── enttest
│   └── enttest.go
├── generate.go
├── hook
│   └── hook.go
├── migrate
│   ├── migrate.go
│   └── schema.go
├── mutation.go
├── predicate
│   └── predicate.go
├── runtime
│   └── runtime.go
├── runtime.go
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user_create.go
├── user_delete.go
├── user.go
├── user_query.go
└── user_update.go

위 처럼 ./ent 폴더내 파일이 생성 됩니다.

테스트

DB에 접속해서 사용해 보겠습니다.

다음 명령어로 테스트 파일을 생성 합니다.

touch main.go
touch todo.db

main.go 를 다음처럼 작성 합니다.

package main

import (
	"context"
	"log"

	"todo/ent"

	// ent가 정의한 user 구조체
	"todo/ent/user"

	// sqlite3 드라이버
	_ "github.com/mattn/go-sqlite3"
)

func main() {

	// sqlite3로 오픈
	client, err := ent.Open("sqlite3", "./todo.db?_fk=1")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ctx := context.Background()

	// 스키마 생성
	if err := client.Schema.Create(ctx); err != nil {
		log.Fatalf("failed creating schema resources: %v", err)
	}

	// user 'tom' 생성
	_, err = client.User.
		Create().
		SetName("tom").
		SetAge(50).
		SetPassword("password1").
		Save(ctx)

	if err != nil {
		log.Fatal(err)
	}

	// 유저 'bob' 생성
	_, err = client.User.
	Create().
	SetName("bob").
	SetAge(85).
	SetPassword("password2").
	Save(ctx)

	if err != nil {
		log.Fatal(err)
	}

	// 유저 'blue' 생성
	_, err = client.User.
	Create().
	SetName("blue").
	SetSubName("sky").
	SetAge(99).
	SetPassword("password").
	Save(ctx)

	if err != nil {
	log.Fatal(err)
	}

	// 나이가 80보다 큰 유저만 쿼리
	users, err := client.User.Query().Where(user.AgeGT(80)).All(ctx)

	if err != nil {
		log.Fatal(err)
	}

	for _, n := range(users) {
		log.Printf("%#v\n", n)
	}

}

터미널 출력

$ go run main.go
... , ID:2, Name:"bob", SubName:"", Age:85, Password:"password2"}
... , ID:3, Name:"blue", SubName:"sky", Age:99, Password:"password"}

Sqlite3

sqlite> select * from users;
1|tom||50|password1
2|bob||85|password2
3|blue|sky|99|password