1. مفاهیم اساسی موجودیت و ارتباط

در چارچوب ent، موجودیت به واحد داده اصلی مدیریت شده در پایگاه داده اشاره دارد که معمولاً مطابق با یک جدول در پایگاه داده است. فیلدها در موجودیت مطابق با ستون‌ها در جدول هستند، در حالی که ارتباطات بین موجودیت‌ها برای توصیف روابط و وابستگی‌های بین موجودیت‌ها استفاده می‌شوند. ارتباط‌های موجودیت‌ها، پایه‌ای برای ساختاردهی مدل‌های داده پیچیده ایجاد می‌کنند و امکان نمایش روابط سلسله مراتبی مانند روابط والدفرزندی و روابط مالکیت را فراهم می‌کنند.

چارچوب ent مجموعه‌ای غنی از رابط‌های برنامه نویسی فراهم کرده است که به توسعه‌دهندگان اجازه می‌دهد تا این ارتباطات را در طرح موجودیت تعریف و مدیریت کنند. از طریق این ارتباطات، می‌توانیم به راحتی منطق تجاری پیچیده بین داده‌ها را بیان و عمل کنیم.

2. انواع ارتباط‌های موجودیت در ent

2.1 ارتباط یک به یک (O2O)

ارتباط یک به یک به تطابق یک به یک بین دو موجودیت اشاره دارد. به عنوان مثال، در مورد کاربران و حساب‌های بانکی، هر کاربر تنها می‌تواند یک حساب بانکی داشته باشد و هر حساب بانکی همچنین تنها به یک کاربر تعلق دارد. چارچوب ent از متدهای edge.To و edge.From برای تعریف چنین ارتباطاتی استفاده می‌کند.

ابتدا، می‌توانیم یک ارتباط یک به یک را به موجودیت «کارت» در طرح موجودیت "کاربر" تعریف کنیم:

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // به موجودیت "کارت" اشاره می‌کند و اسم ارتباط را به عنوان "card" تعریف می‌کند
            Unique(),               // متد Unique اطمینان می‌دهد که این یک ارتباط یک به یک است
    }
}

سپس، ما ارتباط برعکس را به "کاربر" در طرح موجودیت «کارت» تعریف می‌کنیم:

func (Card) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // به "کاربر" از "کارت" اشاره می‌کند و اسم ارتباط را به عنوان "owner" تعریف می‌کند
            Ref("card").              // متد Ref نام ارتباط برعکس متناظر را مشخص می‌کند
            Unique(),                 // با علامت گذاری به عنوان منحصر به فرد، اطمینان حاصل می‌شود که یک کارت با یک صاحب مرتبط است
    }
}

2.2 ارتباط یک به چند (O2M)

ارتباط یک به چند نشان دهنده این است که یک موجودیت ممکن است با چند موجودیت دیگر ارتباط داشته باشد، اما این موجودیت‌ها تنها می‌توانند به یک موجودیت بازگردند. به عنوان مثال، یک کاربر ممکن است چند حیوان خانگی داشته باشد، اما هر حیوان خانگی تنها یک مالک دارد.

در ent، ما همچنان از edge.To و edge.From برای تعریف این نوع ارتباطات استفاده می‌کنیم. مثال زیر ارتباط یک به چند بین کاربران و حیوانات خانگی را تعریف می‌کند:

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("pets", Pet.Type), // ارتباط یک به چند از موجودیت "کاربر" به موجودیت "حیوان خانگی"
    }
}

در موجودیت "حیوان خانگی"، ما یک ارتباط چند به یک به "کاربر" را تعریف می‌کنیم:

func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // ارتباط چند به یک از "حیوان خانگی" به "کاربر"
            Ref("pets").              // اسم ارتباط برعکس از حیوان خانگی به مالک را مشخص می‌کند
            Unique(),                 // اطمینان حاصل می‌شود که یک مالک می‌تواند چند حیوان خانگی داشته باشد
    }
}

2.3 ارتباط چند به چند (M2M)

ارتباط چند به چند اجازه می‌دهد که دو نوع موجودیت می‌توانند دارای چندین نمونه از هم باشند. به عنوان مثال، یک دانشجو می‌تواند در چندین درس ثبت نام کند و یک درس نیز می‌تواند دارای چندین دانشجو باشد. ent، یک API برای ایجاد ارتباط‌های چند به چند فراهم می‌کند:

در موجودیت "دانشجو"، ما از edge.To برای ایجاد ارتباط چند به چند با "درس" استفاده می‌کنیم:

func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // ایجاد یک ارتباط چند به چند از "دانشجو" به "درس"
    }
}

به طریق مشابه، در موجودیت "درس" ما ارتباط برعکس را برای رابطه چند به چند با "دانشجو" ایجاد می‌کنیم:

func (Course) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("students", Student.Type). // ایجاد یک ارتباط چند به چند از "درس" به "دانشجو"
            Ref("courses"),                  // نام ارتباط برعکس از "درس" به "دانشجو" را مشخص می‌کند
    }
}

این انواع ارتباطات، کنشگر اصلی ساختاردهی مدل‌های داده پیچیده برنامه هستند و فهم اینکه چگونه آن‌ها را در 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). // نمونه کاربر بر اساس شناسه کاربر را دریافت کنید
        QueryPets().      // موجودیت‌های حیوان خانگی مرتبط با کاربر را پرس و جو کنید
        All(ctx)          // تمام موجودیت‌های حیوان خانگی پرس و جو شده را برگردانید
    if err != nil {
        return nil, err
    }

    return pets, nil
}

در نمونه کد بالا، ابتدا نمونه کاربر بر اساس شناسه کاربر را دریافت می‌کنیم، سپس از متد QueryPets برای بازیابی تمام موجودیت‌های حیوان خانگی مرتبط با آن کاربر استفاده می‌کنیم.

توجه: ابزار تولید کد ent به طور خودکار API مربوط به پرس و جوی روابط را براساس روابط موجودیت تعریف شده تولید می‌کند. توصیه می‌شود کد تولید شده را مرور کنید.

4. بارگذاری فوری

4.1 اصول پیش‌بارگذاری

پیش‌بارگذاری یک تکنیک استفاده شده در پرس و جو کردن پایگاه داده برای بارگذاری موجودیت‌های مرتبط به صورت پیشین. این رویکرد به صورت معمول برای بدست آوردن داده‌های مرتبط با چند موجودیت به یک‌باره استفاده می‌شود، به منظور جلوگیری از اجرای عملیات پرس و جوی چندین بار بر روی پایگاه داده در پردازش‌های بعدی، و به‌ویژه افزایش قابل ملاحظه‌ای در عملکرد برنامه منجر می‌شود.

در چارچوب ent، پیش‌بارگذاری اصولا برای مدیریت روابط بین موجودیت‌ها، مانند یک به بیش از یک و بیش به بیش، استفاده می‌شود. هنگام بازیابی یک موجودیت از پایگاه داده، موجودیت‌های مرتبط آن به‌صورت خودکار بارگذاری نمی‌شوند. به جای آن، آنها بطور صریح بسته به نیاز از طریق پیش‌بارگذاری بارگذاری می‌شوند. این برای کاهش مسأله پرس و جوی N+1 (به این معنی که برای هر موجودیت والد عملیات پرس و جوی جداگانه انجام می‌شود) بسیار حیاتی است.

در چارچوب ent، پیش‌بارگذاری از طریق استفاده از متد With در سازنده پرس و جو به‌دست می‌آید. این متد توابع متناظر With... را برای هر دیج میلی مانند WithGroups و WithPets تولید می‌کند. این متدها به‌صورت خودکار توسط چارچوب ent تولید می‌شوند و برنامه‌نویسان می‌توانند از آنها برای درخواست پیش‌بارگذاری انجمن‌های خاص استفاده کنند.

اصل کار انجام عملیات پیش‌بارگذاری موجودیت‌ها این است که هنگام پرس و جوی موجودیت اولیه، ent عملیات پرس و جوهای اضافی را برای بازیابی تمام موجودیت‌های مرتبط انجام می‌دهد. سپس این موجودیت‌ها در فیلد Edges شیء برگشتی قرار داده می‌شوند. این به معنی است که ent ممکن است اجرای چندین عملیات پایگاه داده را داشته باشد، حداقل یکبار برای هر لبه مرتبط که باید پیش‌بارگذاری شود. اگرچه این روش ممکن است در برخی حالات کم‌کارایی‌تر از یک پرس و جو اجرایی پیچیده JOIN باشد، اما امکانات بیشتری ارائه می‌دهد و انتظار می‌رود در نسخه‌های آینده ent بهینه‌سازی‌های عملکردی را دریافت کند.

4.2 اجرای عملیات پیش‌بارگذاری

حالا ما نشان می‌دهیم چگونه عملیات پیش‌بارگذاری را در چارچوب ent از طریق چند نمونه کد انجام دهیم، با استفاده از مدل‌های کاربران و حیوانات خانگی توضیح داده‌شده در مرور.

پیش‌بارگذاری یک انجمن تکمیلی

فرض کنید می‌خواهیم تمام کاربران را از پایگاه داده بازیابی کنیم و اطلاعات حیوان خانگی‌ها را پیش‌بارگذاری کنیم. این کار از طریق نوشتن کد زیر انجام می‌شود.

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 چقدر قدرتمند و انعطاف‌پذیر است. با تنها چند تماس متد ساده، می‌تواند اطلاعات تو در تو مرتبط و آنها را به یک شکل ساختاری پیش‌بارگذاری کند. این امکانات بسیار مناسبی برای توسعه اپلیکیشن‌های مبتنی بر داده فراهم می‌کند.