1. مقدمة حول ent

Ent هو إطار كيانات تم تطويره من قبل فيسبوك خصيصًا للغة Go. يبسط عملية بناء وصيانة تطبيقات نموذج البيانات على نطاق كبير. يتبع إطار ent بشكل أساسي المبادئ التالية:

  • تصميم بنية قاعدة بيانات بشكل سهل باعتبارها هيكل رسم بياني.
  • تعريف البنية العامة بأسلوب لغة Go.
  • تنفيذ أنواع مثابتة بناءً على توليد الشفرة.
  • كتابة استعلامات قاعدة بيانات ومرور الرسوم البيانية بشكل بسيط جدًا.
  • إمكانية التوسيع والتخصيص بسهولة باستخدام قوالب Go.

2. إعداد البيئة

لبدء استخدام إطار ent، تأكد من تثبيت لغة Go في بيئة التطوير الخاصة بك.

إذا كانت مجلد مشروعك خارج GOPATH، أو إذا كنت غير معتاد على GOPATH، يمكنك استخدام الأمر التالي لإنشاء مشروع وحدة Go جديد:

go mod init entdemo 

سيقوم هذا بتهيئة وحدة Go جديدة وإنشاء ملف go.mod جديد لمشروع entdemo الخاص بك.

3. تحديد البنية الأولى

3.1. إنشاء البنية باستخدام أداة ent CLI

أولاً، تحتاج إلى تشغيل الأمر التالي في الدليل الجذري لمشروعك لإنشاء بنية ب اسم User باستخدام أداة ent CLI:

go run -mod=mod entgo.io/ent/cmd/ent new User

سيقوم الأمر السابق بتوليد بنية User في دليل entdemo/ent/schema/:

ملف entdemo/ent/schema/user.go:

package schema

import "entgo.io/ent"

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

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

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

3.2. إضافة الحقول

بعد ذلك، نحتاج إلى إضافة تعريفات الحقل إلى بنية المستخدم. فيما يلي مثال على إضافة حقلين إلى كيان المستخدم.

الملف المعدل entdemo/ent/schema/user.go:

package schema

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

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age").
            Positive(),
        field.String("name").
            Default("unknown"),
    }
}

يعرف هذا الكود حقلين لنموذج المستخدم: age و name، حيث يكون age عدد صحيح إيجابي و name سلسلة نصية بقيمة افتراضية "unknown".

3.3. توليد كيانات قاعدة البيانات

بعد تعريف البنية، تحتاج إلى تشغيل أمر go generate لتوليد منطق وصول قاعدة البيانات الأساسي.

قم بتشغيل الأمر التالي في الدليل الجذري لمشروعك:

go generate ./ent

سيؤدي هذا الأمر إلى توليد الشفرة Go المقابلة بناءً على البنية التي تم تعريفها بالفعل، مما يؤدي إلى هيكل الملف التالي:

ent
├── client.go
├── config.go
├── context.go
├── ent.go
├── generate.go
├── mutation.go
... (تم حذف عدة ملفات للانتهاء)
├── schema
│   └── user.go
├── tx.go
├── user
│   ├── user.go
│   └── where.go
├── user.go
├── user_create.go
├── user_delete.go
├── user_query.go
└── user_update.go

4.1. تهيئة اتصال قاعدة البيانات

لإنشاء اتصال بقاعدة بيانات MySQL، يمكننا استخدام الدالة Open المقدمة من إطار العمل ent. أولاً، قم بإستيراد مشغل MySQL ثم قدم سلسلة الاتصال الصحيحة لتهيئة اتصال قاعدة البيانات.

package main

import (
    "context"
    "log"

    "entdemo/ent"
    
    _ "github.com/go-sql-driver/mysql" // استيراد مشغل MySQL
)

func main() {
    // استخدام ent.Open لإنشاء اتصال مع قاعدة بيانات MySQL.
    // تذكر أن تقوم بتغيير العناصر المكانية "اسم المستخدم الخاص بك"، "كلمة السر الخاصة بك"، و "قاعدة البيانات الخاصة بك" بالقيم الصحيحة.
    client, err := ent.Open("mysql", "اسم_المستخدم_الخاص_بك:كلمة_السر_الخاصة_بك@tcp(localhost:3306)/قاعدة_البيانات_الخاصة_بك?parseTime=True")
    if err != nil {
        log.Fatalf("فشل في فتح الاتصال بقاعدة البيانات MySQL: %v", err)
    }
    defer client.Close()

    // تشغيل أداة الهجرة التلقائية
    ctx := context.Background()
    if err := client.Schema.Create(ctx); err != nil {
        log.Fatalf("فشل في إنشاء موارد الهيكل: %v", err)
    }
    
    // يمكن كتابة منطق العمل الإضافي هنا
}

4.2. إنشاء كيانات

إنشاء كيان المستخدم يشمل بناء كيان جديد وحفظه في قاعدة البيانات باستخدام الدالة Save أو SaveX. يُظهر الكود التالي كيفية إنشاء كيان مستخدم جديد وتهيئة حقلي "age" و "name".

// تُستخدم دالة CreateUser لإنشاء كيان مستخدم جديد
func CreateUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // استخدام client.User.Create() لبناء الطلب لإنشاء مستخدم،
    // ثم ربط طرق SetAge و SetName لتعيين قيم حقول الكيان.
    u, err := client.User.
        Create().
        SetAge(30).    // تعيين عمر المستخدم
        SetName("a8m"). // تعيين اسم المستخدم
        Save(ctx)     // استدعاء Save لحفظ الكيان في قاعدة البيانات
    if err != nil {
        return nil, fmt.Errorf("فشل في إنشاء المستخدم: %w", err)
    }
    log.Println("تم إنشاء المستخدم: ", u)
    return u, nil
}

يمكنك في الدالة main استدعاء الدالة CreateUser لإنشاء كيان مستخدم جديد.

func main() {
    // ...تم حذف كود إنشاء اتصال بقاعدة البيانات

    // إنشاء كيان مستخدم
    u, err := CreateUser(ctx, client)
    if err != nil {
        log.Fatalf("فشل في إنشاء المستخدم: %v", err)
    }
    log.Printf("تم إنشاء المستخدم: %#v\n", u)
}

4.3. استعلام الكيانات

لاستعلام الكيانات، يمكننا استخدام بناء الاستعلام الذي يتم توليده بواسطة ent. يُظهر الكود التالي كيفية استعلام مستخدم يحمل الاسم "a8m":

// تُستخدم دالة QueryUser لاستعلام كيان المستخدم بالاسم المحدد
func QueryUser(ctx context.Context, client *ent.Client) (*ent.User, error) {
    // استخدام client.User.Query() لبناء الاستعلام للمستخدم،
    // ثم ربط طريقة Where لإضافة شروط الاستعلام، مثل الاستعلام بواسطة اسم المستخدم
    u, err := client.User.
        Query().
        Where(user.NameEQ("a8m")).      // إضافة شرط الاستعلام، في هذه الحالة، الاسم هو "a8m"
        Only(ctx)                      // تشير طريقة Only إلى أنه يتوقع فقط نتيجة واحدة
    if err != nil {
        return nil, fmt.Errorf("فشل في استعلام المستخدم: %w", err)
    }
    log.Println("تم إرجاع المستخدم: ", u)
    return u, nil
}

يمكنك في الدالة main استدعاء الدالة QueryUser لاستعلام كيان المستخدم.

func main() {
    // ...تم حذف كود إنشاء اتصال بقاعدة البيانات وكود إنشاء مستخدم

    // استعلام كيان المستخدم
    u, err := QueryUser(ctx, client)
    if err != nil {
        log.Fatalf("فشل في استعلام المستخدم: %v", err)
    }
    log.Printf("المستخدم المستعلم: %#v\n", u)
}

5.1. Comprehending Edges and Inverse Edges

In the ent framework, the data model appears as a graph structure, where entities embody nodes in the graph, and the connections between entities are illustrated by edges. An edge denotes a connection from one entity to another; for example, a User can possess numerous Cars.

Inverse Edges, on the other hand, symbolize reverse references to edges, logically presenting the reciprocal relationship between entities without creating a new relationship in the database. For instance, utilizing the inverse edge of a Car, we can identify the User who possesses this car.

The main significance of edges and inverse edges lies in facilitating easy and straightforward navigation between associated entities.

Tip: In ent, edges correspond to traditional database foreign keys and are utilized to define relationships between tables.

5.2. Defining Edges in the Schema

Initially, we will employ the ent CLI to generate the initial schema for Car and Group:

go run -mod=mod entgo.io/ent/cmd/ent new Car Group

Subsequently, in the User schema, we specify the edge with Car to represent the connection between users and cars. We can introduce an edge cars pointing to the Car type in the user entity, indicating that a user can possess multiple cars:

// entdemo/ent/schema/user.go

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("cars", Car.Type),
    }
}

Following the definition of edges, it is essential to execute go generate ./ent again to generate the corresponding code.

5.3. Working with Edge Data

Creating cars associated with a user is a straightforward procedure. Given a user entity, we can create a new car entity and link it with the user:

import (
    "context"
    "log"
    "entdemo/ent"
    // Make sure to import the schema definition for Car
    _ "entdemo/ent/schema"
)

func CreateCarsForUser(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("failed getting user: %v", err)
        return err
    }

    // Create a new car and associate it with the user
    _, err = client.Car.
        Create().
        SetModel("Tesla").
        SetRegisteredAt(time.Now()).
        SetOwner(user).
        Save(ctx)
    if err != nil {
        log.Fatalf("failed creating car for user: %v", err)
        return err
    }

    log.Println("car was created and associated with the user")
    return nil
}

Similarly, retrieving a user's cars is uncomplicated. If we aim to obtain a list of all the cars owned by a user, we can execute the following:

func QueryUserCars(ctx context.Context, client *ent.Client, userID int) error {
    user, err := client.User.Get(ctx, userID)
    if err != nil {
        log.Fatalf("failed getting user: %v", err)
        return err
    }

    // Query all cars owned by the user
    cars, err := user.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("failed querying cars: %v", err)
        return err
    }

    for _, car := range cars {
        log.Printf("car: %v, model: %v", car.ID, car.Model)
    }
    return nil
}

Through the aforementioned steps, we have not only comprehended how to define edges in the schema but also illustrated how to create and retrieve data related to edges.

6. Graph Traversal and Querying

6.1. Grasping Graph Structures

In ent, graph structures are epitomized by entities and the edges connecting them. Every entity equates to a node in the graph, and the relationships between entities are depicted by edges, which can be one-to-one, one-to-many, many-to-many, etc. This graph structure simplifies complex queries and operations on a relational database, making them simple and intuitive.

6.2. تصفح هياكل الرسوم البيانية

كتابة رمز تصفح الرسم البياني يتضمن في الأساس الاستعلام عن البيانات وربطها من خلال الحواف بين الكيانات. فيما يلي مثال بسيط يوضح كيفية تصفح هيكل الرسم البياني في ent:

import (
    "context"
    "log"

    "entdemo/ent"
)

// GraphTraversal هو مثال على تصفح هيكل الرسم البياني
func تصفحالرسم(ctx context.Context, client *ent.Client) error {
    // الاستعلام عن المستخدم المسمى "أرييل"
    a8m, err := client.User.Query().Where(user.NameEQ("Ariel")).Only(ctx)
    if err != nil {
        log.Fatalf("فشل الاستعلام عن المستخدم: %v", err)
        return err
    }

    // تصفح كل السيارات التابعة لأرييل
    cars, err := a8m.QueryCars().All(ctx)
    if err != nil {
        log.Fatalf("فشل الاستعلام عن السيارات: %v", err)
        return err
    }
    for _, car := range cars {
        log.Printf("أرييل يمتلك سيارة من طراز: %s", car.Model)
    }

    // تصفح كل الفرق التي ينتمي إليها أرييل
    groups, err := a8m.QueryGroups().All(ctx)
    if err != nil {
        log.Fatalf("فشل الاستعلام عن الفرق: %v", err)
        return err
    }
    for _, g := range groups {
        log.Printf("أرييل عضو في الفريق: %s", g.Name)
    }

    return nil
}

الرمز أعلاه هو مثال أساسي على تصفح الرسم البياني، حيث يقوم أولاً بالاستعلام عن مستخدم ثم يتصفح سيارات وفرق المستخدم.

7. تصور مخطط قاعدة البيانات

7.1. تثبيت أداة أطلس

لتصور مخطط قاعدة البيانات الذي تم إنشاؤه بواسطة ent، يمكننا استخدام أداة أطلس. خطوات التثبيت لأطلس بسيطة جدًا. على سبيل المثال، يمكنك تثبيتها في نظام macOS باستخدام brew:

brew install ariga/tap/atlas

ملاحظة: أطلس هي أداة ترحيل قاعدة البيانات العالمية القادرة على إدارة إصدار هيكل الجدول لمختلف قواعد البيانات. سيتم تقديم مقدمة مفصلة حول أطلس في فصول لاحقة.

7.2. إنشاء رسم العلاقات الكيانات (ERD) وهيكل SQL

استخدام أطلس لعرض وتصدير الأنظمة بسيط للغاية. بعد تثبيت أطلس، يمكنك استخدام الأمر التالي لعرض رسم العلاقات الكيانات (ERD):

atlas schema inspect -d [database_dsn] --format dot

أو يمكنك بشكل مباشر إنشاء هيكل SQL:

atlas schema inspect -d [database_dsn] --format sql

حيث [database_dsn] يشير إلى اسم مصدر البيانات (DSN) لقاعدة البيانات الخاصة بك. على سبيل المثال، بالنسبة لقاعدة بيانات SQLite، قد تكون كالتالي:

atlas schema inspect -d "sqlite://file:ent.db?mode=memory&cache=shared" --format dot

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

8. ترحيل الهيكل

8.1. ترحيل تلقائي وترحيل متصدر بالإصدارات

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

نصيحة: بالنسبة للترحيل التلقائي، راجع محتوى القسم 4.1.

8.2. تنفيذ ترحيل بالإصدارات

يتضمن عملية الترحيل بالإصدارات إنشاء ملفات الترحيل من خلال أطلس. فيما يلي الأوامر ذات الصلة:

لإنشاء ملفات الترحيل:

atlas migrate diff -d ent/schema/path --dir migrations/dir

بعد ذلك، يمكن تطبيق ملفات الترحيل هذه على قاعدة البيانات:

atlas migrate apply -d migrations/dir --url database_dsn

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

نصيحة: راجع رمز العينة في https://github.com/ent/ent/tree/master/examples/start