1. المفاهيم الأساسية للكيان والرابطة

في إطار ent، يُشير الكيان إلى الوحدة الأساسية للبيانات التي يتم إدارتها في قاعدة البيانات، والتي عادة ما تتوافق مع جدول في قاعدة البيانات. تتوافق الحقول في الكيان مع الأعمدة في الجدول، في حين تُستخدم الرابطات (الحواف) بين الكيانات لوصف العلاقات والتبعيات بين الكيانات. تشكل روابط الكيانات أساسًا لبناء نماذج البيانات المعقدة، مما يسمح بتمثيل العلاقات الهرمية مثل العلاقات الأب-ابن والعلاقات الخاصة بالملكية.

إطار ent يوفر مجموعة غنية من واجهات برمجة التطبيقات، مما يتيح للمطورين تعريف وإدارة هذه الرابطات في مخطط الكيان. من خلال هذه الرابطات، يمكننا التعبير بسهولة والعمل على المنطق التجاري المعقد بين البيانات.

2. أنواع روابط الكيان في ent

2.1 رابطة واحد-إلى-واحد (O2O)

تُشير رابطة واحد-إلى-واحد إلى التوافق الواحد-إلى-واحد بين كيانين. على سبيل المثال، في حالة المستخدمين وحسابات البنوك، يمكن لكل مستخدم الحصول فقط على حساب مصرفي واحد، وكل حساب بنكي ينتمي أيضًا إلى مستخدم واحد فقط. يستخدم إطار ent الأساليب edge.To و edge.From لتحديد مثل هذه الرابطات.

أولاً، يمكننا تحديد رابطة واحد-إلى-واحد تشير إلى Card داخل مخطط User:

// حواف المستخدم.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("card", Card.Type). // يشير إلى كيان البطاقة، ويعرف اسم الرابطة ك "card"
            Unique(),               // تضمن الأسلوب Unique أن هذه هي رابطة واحد-إلى-واحد
    }
}

بعد ذلك، نقوم بتحديد الرابطة العكسية التي تعود إلى User ضمن مخطط Card:

// حواف البطاقة.
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), // رابطة واحد-إلى-عدة من كيان المستخدم إلى كيان الحيوان الأليف
    }
}

في كيان Pet، نحدد رابطة كثير-إلى-واحد مع العودة إلى User:

// حواف الحيوان الأليف.
func (Pet) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("owner", User.Type). // رابطة كثير-إلى-واحد من الحيوان الأليف إلى المستخدم
            Ref("pets").              // يُحدد اسم الرابطة العكسية من الحيوان الأليف إلى المالك
            Unique(),                 // يضمن أن لدى مالك واحد عدة حيوانات أليفة
    }
}

2.3 رابطة كثير-إلى-كثير (M2M)

تسمح رابطة كثير-إلى-كثير بإمكانيّة وجود مثيلات متعددة لنوعين من الكيانات. على سبيل المثال، يمكن لطالب أن يسجل في عدة دورات، ويمكن أيضاً لدورة أن تحتوي على عدة طلاب مسجلين. يوفر ent واجهة برمجة التطبيقات لإقامة روابط كثير-إلى-كثير:

في كيان Student، نستخدم edge.To لإقامة رابطة كثير-إلى-كثير مع Course:

// حواف الطالب.
func (Student) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("courses", Course.Type), // يحدد رابطة كثير-إلى-كثير من الطالب إلى الدورة
    }
}

بالمثل، في كيان Course، نقوم بإقامة رابطة عكسية إلى Student للعلاقة كثير-إلى-كثير:

// حواف الدورة.
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". من خلال بضع مكالمات بسيطة للطرق، يمكنه تحميل بيانات مرتبطة غنية وتنظيمها بطريقة منظمة. يوفر هذا راحة كبيرة لتطوير تطبيقات قائمة على البيانات.