1. Entity(엔터티)와 Association(연결)의 기본 개념
ent
프레임워크에서 엔터티는 일반적으로 데이터베이스의 테이블에 해당하는 기본 데이터 단위를 나타내며, 엔터티의 필드는 테이블의 열에 해당합니다. 엔터티 간의 연결(엣지)은 엔터티 간의 관계와 의존성을 표현하는 데 사용됩니다. 엔터티 연결은 부모-자식 관계 및 소유권 관계와 같은 계층적인 관계를 나타내는 데 기초를 제공합니다.
ent
프레임워크는 다양한 API를 제공하여 개발자가 엔터티 스키마에서 이러한 연결을 정의하고 관리할 수 있습니다. 이러한 연결을 통해 데이터 간의 복잡한 비즈니스 로직을 쉽게 표현하고 조작할 수 있습니다.
2. ent
에서의 엔터티 연결 유형
2.1 일대일(O2O) 연결
일대일 연결은 두 엔터티 간의 일대일 대응을 나타내는 것을 의미합니다. 예를 들어 사용자와 은행 계좌의 경우, 각 사용자는 하나의 은행 계좌만 가질 수 있으며, 각 은행 계좌도 하나의 사용자에만 속합니다. ent
프레임워크는 이러한 연결을 정의하기 위해 edge.To
및 edge.From
메서드를 사용합니다.
첫째, User
스키마 내에서 Card
를 가리키는 일대일 연결을 정의할 수 있습니다:
// User의 엣지(링크).
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("card", Card.Type). // "card"라는 연결 이름을 정의하고 Card 엔터티를 가리킴
Unique(), // Unique 메서드는 이를 일대일 연결로 보장
}
}
다음으로, Card
스키마 내에서 User
로의 역 연결을 정의할 수 있습니다:
// Card의 엣지(링크).
func (Card) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type). // Card에서 User로 연결, 연결 이름은 "owner"로 정의
Ref("card"). // Ref 메서드는 해당 역 연결 이름을 지정
Unique(), // 하나의 카드가 하나의 소유자에 속함을 보장하기 위해 Unique로 표시
}
}
2.2 일대다(O2M) 연결
일대다 연결은 하나의 엔터티가 여러 다른 엔터티와 관련할 수 있지만, 이러한 엔터티들은 하나의 엔터티로 돌아올 수 있다는 것을 나타냅니다. 예를 들어 사용자는 여러 애완동물을 가질 수 있지만, 각 애왙동물은 하나의 주인만을 가질 수 있습니다.
ent
에서도 여전히 edge.To
및 edge.From
을 사용하여 이러한 유형의 연결을 정의합니다. 아래 예시는 사용자와 애완동물 간의 일대다 연결을 정의합니다:
// User의 엣지(링크).
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type), // 사용자 엔터티에서 애완동물 엔터티로의 일대다 연결
}
}
Pet
엔터티에서는 User
로의 일대다 역 연결을 정의합니다:
// Pet의 엣지(링크).
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owener", User.Type). // Pet에서 User로의 일대다 연결
Ref("pets"). // 애완동물에서 주인으로의 역 연결 이름을 지정
Unique(), // 하나의 주인이 여러 애완동물을 가질 수 있도록 Unique로 설정
}
}
2.3 다대다(M2M) 연결
다대다 연결은 두 유형의 엔터티가 서로의 여러 인스턴스를 가질 수 있는 관계를 나타냅니다. 예를 들어 학생은 여러 과목에 등록할 수 있으며, 과목도 여러 학생이 등록될 수 있습니다. ent
은 다대다 연결을 설정하기 위한 API를 제공합니다:
Student
엔터티에서 Course
와의 다대다 연결을 정의하기 위해 edge.To
를 사용합니다:
// Student의 엣지(링크).
func (Student) Edges() []ent.Edge {
return []ent.Edge{
edge.To("courses", Course.Type), // Student에서 Course로의 다대다 연결 정의
}
}
마찬가지로, Course
엔터티에서는 다대다 관계에 대한 Student
로의 역 연결을 설정합니다:
// Course의 엣지(링크).
func (Course) Edges() []ent.Edge {
return []ent.Edge{
edge.From("students", Student.Type). // Course에서 Student로의 다대다 연결 정의
Ref("courses"), // Course에서 Student로의 역 연결 이름 지정
}
}
이러한 유형의 연결은 복잡한 응용프로그램 데이터 모델을 구축하는 데 중요하며, ent
에서 이를 어떻게 정의하고 사용하는지에 대한 이해는 데이터 모델 및 비즈니스 로직을 확장하는 데 중요합니다.
3. 엔터티 연관성에 대한 기본 작업
이 섹션에서는 ent
를 사용하여 정의된 관계를 이용하여 기본 작업을 수행하는 방법을 보여줄 것입니다. 이는 엔티티를 생성하고, 쿼리하고, 연관된 엔티티를 탐색하는 것을 포함합니다.
3.1 연관된 엔티티 생성
엔티티를 생성할 때, 동시에 엔티티 간의 관계를 설정할 수 있습니다. 일대다 (O2M) 및 다대다 (M2M) 관계의 경우 Add{Edge}
메서드를 사용하여 연관된 엔티티를 추가할 수 있습니다.
예를 들어, 사용자 엔티티와 애완동물 엔티티가 특정 연관을 가지는 경우, 사용자가 여러 애완동물을 가질 수 있는 경우를 가정해 보겠습니다. 다음은 새로운 사용자를 생성하고 해당 사용자에게 애완동물을 추가하는 예입니다:
// 사용자 생성 및 애완동물 추가
func CreateUserWithPets(ctx context.Context, client *ent.Client) (*ent.User, error) {
// 애완동물 인스턴스 생성
fido := client.Pet.
Create().
SetName("피도").
SaveX(ctx)
// 사용자 인스턴스를 생성하고 애완동물과 연관시킵니다
user := client.User.
Create().
SetName("앨리스").
AddPets(fido). // 애완동물을 연관시키기 위해 AddPets 메서드를 사용합니다
SaveX(ctx)
return user, nil
}
이 예에서는 먼저 "피도"라는 애완동물 인스턴스를 생성한 다음, "앨리스"라는 사용자를 생성하고 AddPets
메서드를 사용하여 애완동물을 사용자에 연관시킵니다.
3.2 연관된 엔티티 쿼리
연관된 엔티티를 쿼리하는 것은 ent
에서 흔한 작업입니다. 예를 들어, Query{Edge}
메서드를 사용하여 특정 엔티티와 연관된 다른 엔티티를 검색할 수 있습니다.
사용자와 애완동물의 예제를 계속하면, 아래는 사용자가 소유한 모든 애완동물을 쿼리하는 방법입니다:
// 사용자의 모든 애완동물 쿼리
func QueryUserPets(ctx context.Context, client *ent.Client, userID int) ([]*ent.Pet, error) {
pets, err := client.User.
Get(ctx, userID). // 사용자 ID를 기반으로 사용자 인스턴스를 가져옴
QueryPets(). // 사용자와 연관된 애완동물 엔티티를 쿼리함
All(ctx) // 쿼리된 모든 애완동물 엔티티를 반환함
if err != nil {
return nil, err
}
return pets, nil
}
위 코드 스니펫에서는 먼저 사용자 ID를 기반으로 사용자 인스턴스를 가져와서, 그 사용자와 연관된 모든 애완동물 엔티티를 가져오기 위해 QueryPets
메서드를 호출합니다.
참고:
ent
의 코드 생성 도구는 정의된 엔티티 관계에 기반한 연관 쿼리를 자동으로 생성합니다. 생성된 코드를 검토하는 것이 권장됩니다.
4. Eager Loading
4.1 Preloading의 원리
Preloading은 데이터베이스 쿼리에서 연관된 엔티티를 미리 가져와 로드하는 기술입니다. 이 접근 방식은 여러 엔티티에 대한 관련 데이터를 한 번에 얻기 위해 일반적으로 사용되며, 후속 처리에서 여러 데이터베이스 쿼리 작업을 피하기 위해 사용되어 애플리케이션의 성능을 크게 향상시킵니다.
ent 프레임워크에서는 preloading이 주로 일대다 및 다대다와 같은 엔티티 간의 관계를 처리하는 데 사용됩니다. 데이터베이스에서 엔티티를 검색할 때, 연관된 엔티티는 자동으로 로드되지 않습니다. 대신 이들은 preloading을 통해 필요할 때 명시적으로 로드됩니다. 이는 N+1 쿼리 문제(즉, 각 부모 엔티티에 대해 별도의 쿼리를 수행)를 해소하는 데 중요합니다.
ent 프레임워크에서 preloading은 쿼리 빌더의 With
메서드를 사용하여 이루어집니다. 이 메서드는 WithGroups
및 WithPets
와 같은 각 엣지에 대한 해당 With...
함수를 생성합니다. 이러한 메서드들은 ent 프레임워크에 의해 자동으로 생성되며 개발자는 이들을 사용하여 특정 연관의 preloading을 요청할 수 있습니다.
엔티티를 preloading하는 작업 원리는 주요 엔티티를 쿼리할 때, ent가 모든 연관된 엔티티를 검색하기 위해 추가적인 쿼리를 실행한다는 것입니다. 이후 이러한 엔티티는 반환된 객체의 Edges
필드에 채워집니다. 이는 ent가 적어도 preloading이 필요한 각 연관 엣지마다 적어도 한 번 이상의 데이터베이스 쿼리를 실행할 수 있는 것을 의미합니다. 이 방법은 특정 시나리오에서 단일 복잡한 JOIN
쿼리보다 효율적이지 않을 수 있지만, 보다 큰 유연성을 제공하며 향후 ent의 성능 최적화를 받을 것으로 기대됩니다.
4.2 Preloading의 구현
이제 사용자와 애완동물의 모델을 사용하여 ent 프레임워크에서 preloading 작업을 수행하는 방법에 대한 예제 코드를 통해 따라해보겠습니다.
단일 연관 로딩
우리가 데이터베이스에서 모든 사용자를 검색하고 애완동물 데이터를 미리 로드하려면 다음과 같은 코드를 작성할 수 있습니다.
users, err := client.User.
Query().
WithPets().
All(ctx)
if err != nil {
// 에러 처리
return err
}
for _, u := range users {
for _, p := range u.Edges.Pets {
fmt.Printf("사용자 (%v)가 애완동물 (%v)을 소유\n", u.ID, p.ID)
}
}
이 예에서는 WithPets
메서드를 사용하여 ent에게 사용자와 관련된 애완동물 엔티티를 미리 로드하도록 요청합니다. 미리로드된 애완동물 데이터는 Edges.Pets
필드에 채워지며, 여기서 이 관련 데이터에 액세스할 수 있습니다.
다중 연관 로딩
ent를 사용하면 한 번에 여러 연관을 미리로드할 수 있으며, 중첩된 연관, 필터링, 정렬 또는 미리로드된 결과 수를 제한하는 것도 가능합니다. 아래는 관리자들의 애완동물 및 그들이 속한 팀을 미리로드하고, 동시에 팀에 속한 사용자들을 미리로드하는 예시입니다.
admins, err := client.User.
Query().
Where(user.Admin(true)).
WithPets().
WithGroups(func(q *ent.GroupQuery) {
q.Limit(5) // 처음 5개 팀으로 제한
q.Order(ent.Asc(group.FieldName)) // 팀 이름을 기준으로 오름차순 정렬
q.WithUsers() // 팀 내 사용자들 미리로드
}).
All(ctx)
if err != nil {
// 에러 처리
return err
}
for _, admin := range admins {
for _, p := range admin.Edges.Pets {
fmt.Printf("관리자 (%v)가 애완동물 (%v)을 소유\n", admin.ID, p.ID)
}
for _, g := range admin.Edges.Groups {
fmt.Printf("관리자 (%v)가 팀 (%v)에 속함\n", admin.ID, g.ID)
for _, u := range g.Edges.Users {
fmt.Printf("팀 (%v)에는 멤버 (%v)가 있음\n", g.ID, u.ID)
}
}
}
이 예제를 통해 ent가 얼마나 강력하고 유연한지 볼 수 있습니다. 몇 가지 간단한 메서드 호출로 풍부한 관련 데이터를 미리로드하고 구조화된 방식으로 구성할 수 있습니다. 이는 데이터 중심 애플리케이션을 개발하는 데 큰 편의를 제공합니다.