1. أساسيات النموذج والحقل

1.1. مقدمة لتعريف النموذج

في إطار ORM ، يُستخدم النموذج لوصف العلاقة التعيينية بين أنواع الكيانات في التطبيق وجداول قاعدة البيانات. يعرف النموذج الخصائص والعلاقات للكيان، فضلاً عن تكوينات قاعدة البيانات المحددة لها. في إطار الـ ent ، يُستخدم النماذج عادةً لوصف أنواع الكيانات في رسم بياني، مثل User أو Group.

تشمل تعريفات النماذج عادةً وصف لحقول (أو الخصائص) الكيان وحواف (أو العلاقات)، فضلاً عن بعض الخيارات المحددة لقاعدة البيانات. يمكن أن تساعد هذه التعريفات في تحديد الهيكل والخصائص والعلاقات للكيان، ويمكن استخدامها لتوليد هيكل جدول قاعدة البيانات المقابل بناءً على النموذج.

1.2. نظرة عامة على الحقول

الحقول هي الجزء من النموذج الذي يمثل خصائص الكيان. تحدد خصائص الكيان، مثل الاسم، والعمر، والتاريخ، إلخ. في إطار الـ ent ، تشمل أنواع الحقول مجموعة متنوعة من أنواع البيانات الأساسية، مثل العدد الصحيح، والسلسلة، والمنطقي، والزمن، إلخ، فضلاً عن بعض أنواع قاعدة البيانات المحددة، مثل UUID، []byte، JSON، إلخ.

توضح الجدول أدناه أنواع الحقول المدعومة بواسطة إطار الـ ent:

النوع الوصف
int نوع العدد الصحيح
uint8 نوع العدد الصحيح غير الموقع
float64 نوع العدد عائم
bool نوع منطقي
string نوع السلسلة
time.Time نوع الزمن
UUID نوع UUID
[]byte نوع مصفوفة البايت (قاعدة بيانات SQL فقط)
JSON نوع JSON (قاعدة بيانات SQL فقط)
Enum نوع تعدادي (قاعدة بيانات SQL فقط)
Other أنواع أخرى (مثل نطاق Postgres)

2. تفاصيل خصائص الحقول

2.1. أنواع البيانات

يحدد نوع البيانات لسمة أو حقل في نموذج الكيان شكل البيانات التي يمكن تخزينها. هذا جزء أساسي من تعريف النموذج في إطار الـ ent. فيما يلي بعض أنواع البيانات المستخدمة بشكل شائع في إطار الـ ent.

import (
    "time"

    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// مخطط المستخدم.
type User struct {
    ent.Schema
}

// حقول المستخدم.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age"),             // نوع العدد الصحيح
        field.String("name"),         // نوع السلسلة
        field.Bool("active"),         // نوع منطقي
        field.Float("score"),         // نوع العدد العائم
        field.Time("created_at"),     // نوع الطابع الزمني
    }
}
  • int: يمثل قيم الأعداد الصحيحة، ويمكن أن يكون int8، int16، int32، int64، إلخ.
  • string: يمثل بيانات السلسلة.
  • bool: يمثل قيم منطقية، وعادةً ما تُستخدم كرايات.
  • float64: يمثل أرقام عائمة، ويمكن أيضًا استخدام float32.
  • time.Time: يمثل الزمن، وعادةً ما تُستخدم للطوابع الزمنية أو بيانات التاريخ.

سيتم تعيين هذه الأنواع من الحقول إلى الأنواع المقابلة المدعومة بواسطة قاعدة البيانات الأساسية. بالإضافة إلى ذلك، يدعم الـ ent أنواعًا أكثر تعقيدًا مثل UUID و JSON وتكوينات مثل تعدادات (Enum)، ودعمًا لأنواع قاعدة بيانات خاصة مثل []byte (قاعدة بيانات SQL فقط) و Other (قاعدة بيانات SQL فقط).

2.2. القيم الافتراضية

يمكن تكوين الحقول بقيم افتراضية. إذا لم يتم تحديد القيمة المقابلة عند إنشاء كيان، سيتم استخدام القيمة الافتراضية. يمكن أن تكون القيمة الافتراضية قيمة ثابتة أو قيمة مُولّدة ديناميكيًا من وظيفة. استخدم الطريقة .Default لتعيين قيمة افتراضية ثابتة، أو استخدم .DefaultFunc لتعيين قيمة افتراضية مُولّدة ديناميكيًا.

// مخطط المستخدم.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Time("created_at").
            Default(time.Now),  // قيمة افتراضية ثابتة لـ time.Now
        field.String("role").
            Default("user"),   // قيمة سلسلة ثابتة
        field.Float("score").
            DefaultFunc(func() float64 {
                return 10.0  // قيمة افتراضية مولدة بواسطة وظيفة
            }),
    }
}

2.3. اختيارية الحقول وقيم الصفر

بشكل افتراضي، الحقول مطلوبة. لتعريف حقل اختياري، استخدم الدالة .Optional(). سيتم تعريف الحقول الاختيارية كحقولٍ قابلة للقيمة الفارغة في قاعدة البيانات. الخيار Nillable يسمح بتعيين قيمٍ nil للحقول، مما يميّز بين قيمة الصفر للحقل والحالة غير المعينة.

// مخطط المستخدم.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // الحقل الاختياري غير مطلوب
        field.Int("age").Optional().Nillable(), // الحقل القابل للقيمة الفارغة يمكن تعيين قيمة `nil`
    }
}

عند استخدام نموذج الذي تم تحديده أعلاه، يمكن لحقل العمر قبول قيم nil للإشارة للحالة غير المعينة، فضلاً عن القيم غير الفارغة.

2.4. فرادة الحقول

تضمن الحقول الفريدة أنه لا توجد قيم مكررة في جدول قاعدة البيانات. استخدم الدالة Unique() لتعريف حقل فريد. عند إنشاء سلامة بيانات كمتطلب حاسم، مثل عناوين البريد الإلكتروني أو أسماء المستخدمين، يجب استخدام الحقول الفريدة.

// مخطط المستخدم.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("email").Unique(),  // حقل فريد لتجنب تكرار عناوين البريد الإلكتروني
    }
}

سيتم إنشاء قيد فريد في قاعدة البيانات الأساسية لمنع إدخال القيم المكررة.

2.5. فهرسة الحقول

تستخدم فهرسة الحقول لتحسين أداء استعلامات قاعدة البيانات، خاصة في قواعد البيانات الكبيرة. في إطار ent، يمكن استخدام الدالة .Indexes() لإنشاء فهارس.

import "entgo.io/ent/schema/index"

// مخطط المستخدم.
func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email"),  // إنشاء فهرس على حقل 'البريد الإلكتروني'
        index.Fields("name", "age").Unique(), // إنشاء فهرس مركب فريد
    }
}

يمكن استخدام الفهارس للحقول التي يتم استعلامها بشكل متكرر، ولكن من المهم ملاحظة أن وجود فهارس كثيرة قد يؤدي إلى انخفاض أداء العمليات الكتابة. لذلك، يجب موازنة القرار بإنشاء الفهارس استنادًا إلى الظروف الفعلية.

2.6. العلامات المخصصة

في إطار ent، يمكنك استخدام الدالة StructTag لإضافة علامات مخصصة إلى حقول هيكل الكيان الناتج. تكون هذه العلامات مفيدة جدًا لتنفيذ العمليات مثل ترميز JSON وترميز XML. في المثال أدناه، سنضيف علامات JSON وXML مخصصة لحقل "الاسم".

// حقول المستخدم.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // إضافة علامات مخصصة باستخدام الدالة StructTag
            // هنا، ضبط العلامة JSON لحقل الاسم على 'اسم المستخدم' وتجاهلها عندما يكون الحقل فارغًا (omitempty)
            // كما، ضبطت العلامة XML للترميز على 'الاسم'
            StructTag(`json:"اسم المستخدم,omitempty" xml:"الاسم"`),
    }
}

عند الترميز باستخدام JSON أو XML، فإن الخيار omitempty يشير إلى أنه إذا كان الحقل "الاسم" فارغًا، فإنه سيتم حذف هذا الحقل من نتيجة الترميز. هذا مفيد جدًا لتقليل حجم جسم الرد عند كتابة الواجهات البرمجية.

هذا يوضح أيضًا كيفية ضبط علامات متعددة لنفس الحقل بشكل متزامن. تستخدم العلامات JSON مفتاح json، والعلامات XML تستخدم مفتاح xml ويتم فصلهم بفواصل. ستُستخدم هذه العلامات في وظائف المكتبة مثل encoding/json و encoding/xml عند تحليل الهيكل للترميز أو فك الترميز.

3. التحقق من صحة الحقول والقيود

التحقق من صحة الحقول هو جانب مهم في تصميم قاعدة البيانات لضمان اتساق البيانات وصحتها. في هذا القسم، سنناقش استخدام المحققين المدمجين، والمحققين المخصصين، ومختلف القيود لتحسين سلامة البيانات وجودتها في نموذج الكيان.

3.1. المحققون المدمجين

يوفر الإطار سلسلة من المحققين المدمجين لأداء فحوصات صحة البيانات الشائعة على أنواع مختلفة من الحقول. يمكن أن يبسط استخدام هؤلاء المحققين المدمجين عملية التطوير وتحديد سريع لنطاقات البيانات الصالحة أو الصيغ للحقول.

فيما يلي بعض أمثلة من محققي الحقول المدمجين:

  • محققون لأنواع الأعداد:
    • Positive(): يحقق ما إذا كانت قيمة الحقل عددًا إيجابيًا.
    • Negative(): يحقق ما إذا كانت قيمة الحقل عددًا سالبًا.
    • NonNegative(): يحقق ما إذا كانت قيمة الحقل عددًا غير سالب.
    • Min(i): يحقق ما إذا كانت قيمة الحقل أكبر من قيمة دنيا معينة i.
    • Max(i): يحقق ما إذا كانت قيمة الحقل أقل من قيمة عظمى معينة i.
  • محققون لنوع string:
    • MinLen(i): يحقق الحد الأدنى لطول السلسلة.
    • MaxLen(i): يحقق الحد الأقصى لطول السلسلة.
    • Match(regexp.Regexp): يحقق ما إذا كانت السلسلة مطابقة للتعبير العادي المعطى.
    • NotEmpty: يحقق ما إذا كانت السلسلة غير فارغة.

لنلق نظرة على مثال عملي للشفرة. في هذا المثال، يتم إنشاء نموذج "User"، الذي يشمل حقلاً للعمر من نوع الأرقام الصحيحة غير السالبة وحقل "email" بصيغة ثابتة:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("email").
            Match(regexp.MustCompile(`^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`)),
    }
}

3.2. المحققون المخصصون

بينما يمكن للمحققين المدمجين التعامل مع العديد من متطلبات التحقق الشائعة، قد تحتاج أحيانًا إلى منطق تحقق معقد أكثر. في مثل هذه الحالات، يمكنك كتابة محققين مخصصين لتلبية قواعد العمل المحددة.

المحقق المخصص هو دالة تستقبل قيمة حقل وتُرجع error. إذا كانت قيمة error المُرجعة غير فارغة، فإن ذلك يشير إلى فشل التحقق. التنسيق العام للمحقق المخصص كما يلي:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("phone").
            Validate(func(s string) error {
                // التحقق ما إذا كان رقم الهاتف يفي بالصيغة المتوقعة
                matched, _ := regexp.MatchString(`^\+?[1-9]\d{1,14}$`, s)
                if !matched {
                    return errors.New("صيغة رقم الهاتف غير صحيحة")
                }
                return nil
            }),
    }
}

كما هو موضح أعلاه، قمنا بإنشاء محقق مخصص للتحقق من صيغة رقم الهاتف.

3.3. القيود

القيود هي قواعد تفرض قواعد معينة على كائن قاعدة البيانات. يمكن استخدامها لضمان صحة واتساق البيانات، مثل منع إدخال بيانات غير صالحة أو تعريف علاقات البيانات.

تشمل القيود الشائعة في قاعدة البيانات ما يلي:

  • قيد المفتاح الأساسي: يضمن أن كل سجل في الجدول فريد.
  • قيد الفريد: يضمن أن قيمة العمود أو مجموعة من الأعمدة فريدة في الجدول.
  • قيد المفتاح الخارجي: يحدد العلاقات بين الجداول، مضمنًا سلامة الإشارة.
  • قيد الفحص: يضمن أن قيمة الحقل تفي بشرط معين.

في نموذج الكيان، يمكنك تعريف القيود للحفاظ على تكامل البيانات على النحو التالي:

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("username").
            Unique(), // قيد فريد لضمان أن اسم المستخدم فريد في الجدول.
        field.String("email").
            Unique(), // قيد فريد لضمان أن البريد الإلكتروني فريد في الجدول.
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("friends", User.Type).
            Unique(), // قيد المفتاح الخارجي، إنشاء علاقة حافة فريدة مع مستخدم آخر.
    }
}

باختصار، تحقق الحقول والقيود أمرٌ حاسم لضمان جودة البيانات الجيدة وتجنب الأخطاء غير المتوقعة في البيانات. يمكن أن يجعل استخدام الأدوات المقدمة من إطار "ent" هذه العملية أكثر بساطة وموثوقية.