1. مبانی مدل و فیلد

1.1. معرفی تعریف مدل

در یک چارچوب ORM، یک مدل برای توصیف رابطه‌ی نگاشت بین انواع موجودیت در برنامه و جداول پایگاه داده استفاده می‌شود. مدل ویژگی‌ها و روابط موجودیت را تعریف می‌کند، همچنین تنظیمات خاص به پایگاه داده مرتبط با آنها را تعریف می‌کند. در چارچوب ent، مدل‌ها معمولاً برای توصیف انواع موجودیت‌ها در یک گراف استفاده می‌شوند، مانند User یا Group.

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

1.2. مرور فیلدها

فیلدها بخشی از مدل هستند که ویژگی‌های موجودیت را نشان می‌دهند. آن‌ها ویژگی‌های موجودیت را مانند نام، سن، تاریخ و غیره تعریف می‌کنند. در چارچوب ent، انواع فیلد شامل انواع داده‌ای ابتدایی مختلف هستند، مانند integer، string، boolean، time و غیره، همچنین برخی از انواع خاص مرتبط با SQL، مانند UUID، []byte، JSON و غیره.

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

نوع توضیح
int نوع Integer
uint8 نوع Integer بدون علامت 8 بیت
float64 نوع اعشاری
bool نوع Boolean
string نوع String
time.Time نوع زمانی
UUID نوع UUID
[]byte نوع آرایه بایت (فقط SQL)
JSON نوع JSON (فقط SQL)
Enum نوع Enum (فقط SQL)
دیگر سایر انواع (مانند Range پستگرس)

2. جزئیات ویژگی‌های فیلد

2.1. انواع داده

نوع داده‌ای یک ویژگی یا فیلد در مدل موجودیت شکل داده‌ای را که می‌تواند ذخیره شود مشخص می‌کند. این بخشی حیاتی از تعریف مدل در چارچوب ent است. در زیر برخی از انواع داده‌ای معمولاً استفاده شده در چارچوب ent آورده شده است.

import (
    "time"

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

// User schema.
type User struct {
    ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.Int("age"),             // Integer type
        field.String("name"),         // String type
        field.Bool("active"),         // Boolean type
        field.Float("score"),         // Floating point type
        field.Time("created_at"),     // Timestamp type
    }
}
  • int: مقادیر integer را نشان می‌دهد که ممکن است int8، int16، int32، int64 باشد.
  • string: داده‌های رشته‌ای را نشان می‌دهد.
  • bool: مقادیر بولین را نشان می‌دهد، معمولاً برای استفاده به عنوان پرچم‌ها استفاده می‌شود.
  • float64: اعداد اعشاری را نشان می‌دهد، همچنین می‌توان از float32 نیز استفاده کرد.
  • time.Time: زمان را نشان می‌دهد، معمولاً برای تمرین‌ها یا داده‌‌های تاریخ استفاده می‌شود.

این انواع فیلد به انواع مرتبط پایگاه داده تبدیل می‌شوند. علاوه بر این، ent از انواع پیچیده‌تر مانند UUID، JSON، enum ها (Enum) و پشتیبانی از انواع خاص پایگاه داده مانند []byte (فقط SQL) و Other (فقط SQL) پشتیبانی می‌کند.

2.2. مقادیر پیش‌فرض

فیلدها می‌توانند با مقادیر پیش‌فرض پیکربندی شوند. اگر مقدار مرتبط هنگام ایجاد یک موجودیت مشخص نشود، مقدار پیش‌فرض استفاده خواهد شد. مقدار پیش‌فرض می‌تواند یک مقدار ثابت یا یک مقدار پویا توسط یک تابع تولید شده باشد. برای تنظیم یک مقدار پیش‌فرض استاتیک از .Default استفاده کنید یا از .DefaultFunc برای تنظیم یک مقدار پیش‌فرض پویا تولید شده استفاده کنید.

// User schema.
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() استفاده کنید. فیلدهای اختیاری به عنوان فیلدهای قابل تأیید (nullable) در پایگاه داده اعلام می‌شوند. گزینه Nillable امکان قرار دادن مقادیر nil برای فیلدها را فراهم می‌کند، که بین مقدار صفر یک فیلد و وضعیت unset یک فیلد امکان تمایز فراهم می‌کند.

// ساختار کاربر.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("nickname").Optional(), // فیلد اختیاری و اجباری نیست
        field.Int("age").Optional().Nillable(), // فیلد قابل تأیید است و می‌تواند به مقدار nil تنظیم شود
    }
}

در صورت استفاده از مدل تعریف‌شده بالا، فیلد age می‌تواند هم مقادیر nil را برای نشان دادن unset قبول کند و هم مقادیر صفری غیر 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"),  // ایجاد یک فهرست روی فیلد 'email'
        index.Fields("name", "age").Unique(), // ایجاد یک فهرست مجموعه‌ای یکتایی
    }
}

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

2.6. برچسب‌های سفارشی

در چارچوب ent، می‌توان از متد StructTag برای اضافه کردن برچسب‌های سفارشی به فیلدهای ساخته‌شده انتیتی استفاده کرد. این برچسب‌ها برای پیاده‌سازی عملیات‌های مانند رمزگذاری JSON و رمزگذاری XML بسیار مفید هستند. در مثال زیر، برچسب‌های سفارشی JSON و XML برای فیلد name اضافه خواهیم کرد.

// فیلدهای کاربر.
func (User) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").
            // اضافه کردن برچسب‌های سفارشی با استفاده از متد StructTag
            // در اینجا، برچسب JSON برای فیلد نام به 'username' تنظیم شده و هنگامی که فیلد خالی است نادیده گرفته می‌شود (omitempty)
            // همچنین، برچسب XML برای رمزگذاری به 'name' تنظیم شده است
            StructTag(`json:"username,omitempty" xml:"name"`),
    }
}

زمانی که با JSON یا XML رمزگشایی می‌کنیم، گزینه omitempty نشان می‌دهد که اگر فیلد نام خالی باشد، آنگاه این فیلد از نتیجه رمزگشایی حذف خواهد شد. این بسیار مفید است برای کاهش اندازه بدنه پاسخ هنگام نوشتن API‌ها.

این نشان دهنده آن است که چگونه می‌توان برای یک فیلد، همزمان چندین برچسب را تنظیم کرد. برچسب‌های JSON از کلید json، برچسب‌های XML از کلید xml استفاده می‌کنند و توسط توابع کتابخانه‌هایی مانند encoding/json و encoding/xml استفاده خواهند شد هنگام تجزیه ساختار برای رمزگشایی یا رمزگذاری.

3. اعتبارسنجی و محدودیت‌های فیلد

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

3.1. اعتبارسنج‌های داخلی

فریم‌ورک این امکان را فراهم می‌کند که یک سری از اعتبارسنج‌های داخلی را برای انجام بررسی‌های معتبریت داده روی انواع مختلف فیلدها ارائه دهد. استفاده از این اعتبارسنج‌های داخلی می‌تواند فرآیند توسعه را ساده‌تر کرده و به سرعت برای فیلدها محدوده‌ها یا فرمت‌های داده معتبر را تعریف کند.

در زیر چند نمونه از اعتبارسنج‌های داخلی فیلد آورده شده‌است:

  • اعتبارسنج‌های اعداد:
    • Positive(): اعتبارسنجی برای بررسی اینکه مقدار فیلد عدد مثبت است یا خیر.
    • Negative(): اعتبارسنجی برای بررسی اینکه مقدار فیلد عدد منفی است یا خیر.
    • NonNegative(): اعتبارسنجی برای بررسی اینکه مقدار فیلد عدد غیرمنفی است یا خیر.
    • Min(i): اعتبارسنجی برای بررسی اینکه مقدار فیلد بزرگتر از یک مقدار حداقل مشخص است یا خیر.
    • Max(i): اعتبارسنجی برای بررسی اینکه مقدار فیلد کوچکتر از یک مقدار حداکثر مشخص است یا خیر.
  • اعتبارسنج‌های نوع string:
    • MinLen(i): اعتبارسنجی برای بررسی حداقل طول یک رشته.
    • MaxLen(i): اعتبارسنجی برای بررسی حداکثر طول یک رشته.
    • Match(regexp.Regexp): اعتبارسنجی برای بررسی مطابقت رشته با یک عبارت منظم داده شده.
    • NotEmpty: اعتبارسنجی برای بررسی اینکه رشته خالی نباشد.

حال کد عملی زیر را مشاهده می‌کنیم. در این مثال، یک مدل User ایجاد شده است، که شامل فیلد اعداد صحیح غیرمنفی age و فیلد 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 را برمی‌گرداند. اگر خطای برگشتی خالی نباشد، بیانگر شکست اعتبارسنجی است. فرمت کلی یک اعتبارسنج سفارشی به صورت زیر است:

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. محدودیت‌ها

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

محدودیت‌های معمول پایگاه داده شامل موارد زیر می‌شود:

  • محدودیت کلید اصلی: مطمئن می‌شود که هر رکورد در جدول منحصر به فرد است.
  • محدودیت یکتایی: مطمئن می‌شود که مقدار یک ستون یا ترکیبی از ستون‌ها در جدول منحصر به فرد است.
  • محدودیت کلید خارجی: روابط بین جداول را تعریف می‌کند و اطمینان می‌یابد از صحت مراجعه.
  • محدودیت بررسی: مطمئن می‌شود که مقدار یک فیلد شرط خاصی را برآورده می‌کند.

در مدل entity، می‌توانید محدودیت‌ها را برای حفظ صحت داده‌ها به شکل زیر تعریف کنید:

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 می‌تواند این فرآیند را ساده‌تر و قابل اعتماد تر کند.