Skip to content

مكتبة Gorm ORM لقواعد البيانات

الوثائق الرسمية: GORM - The fantastic ORM library for Golang, aims to be developer friendly.

المستودع المفتوح: go-gorm/gorm: The fantastic ORM library for Golang, aims to be developer friendly (github.com)

في مجتمع Go، بالنسبة للتفاعل مع قواعد البيانات، هناك فريقان: فريق يفضل مكتبات بسيطة مثل sqlx، التي لا تكون وظائفها قوية جدًا لكنها تسمح بالتحكم الكامل في SQL وتحسين الأداء لأقصى حد. والفريق الآخر يحب ORM الذي يُولد من أجل كفاءة التطوير، مما يوفر الكثير من المتاعب أثناء عملية التطوير. وعند الحديث عن ORM في مجتمع Go، لا يمكن تجاوز gorm، وهو ORM عريق جدًا، ومن المكتبات المشابهة له xorm و ent الأحدث نسبيًا. هذا المقال يتحدث عن محتوى gorm، وهو مجرد شرح للمحتوى الأساسي للمبتدئين، ولمزيد من التفاصيل يمكن قراءة الوثائق الرسمية، التي أصبحت وثائقها الصينية مكتملة جدًا.

الخصائص

  • ORM كامل الوظائف
  • العلاقات (واحد لواحد، واحد لكثير، ينتمي إلى، كثير لكثير، متعدد الأشكال، وراثة الجدول الواحد)
  • دوال الخطاف في Create، Save، Update، Delete، Find
  • دعم التحميل المسبق عبر Preload و Joins
  • المعاملات، المعاملات المتداخلة، نقاط الحفظ، التراجع إلى نقطة الحفظ
  • Context، وضع الترجمة المسبقة، وضع DryRun
  • الإدراج الدفعي، FindInBatches، Find/Create with Map، استخدام تعابير SQL و Context Valuer للعمليات
  • منشئ SQL، Upsert، الأقفال، تلميحات المحسن/الفهرس/التعليقات، المعاملات المسماة، الاستعلامات الفرعية
  • المفاتيح الأساسية المركبة، الفهارس، القيود
  • الترحيل التلقائي
  • مسجل مخصص
  • واجهة برمجة إضافية مرنة وقابلة للتوسعة: Database Resolver (قواعد بيانات متعددة، فصل القراءة والكتابة)، Prometheus...
  • كل ميزة تم اختبارها بشكل مكثف
  • سهل الاستخدام للمطورين

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

كبديل، هناك مكتبتا ORM يمكن تجربتهما: الأولى هي aorm، التي افتُتحت مؤخرًا، ولا تحتاج إلى كتابة أسماء حقول الجدول يدويًا، ومعظم العمليات متسلسلة، وتعتمد على الانعكاس، وبما أن عدد النجوم قليل، يمكن الانتظار والمتابعة. الثانية هي ent، التي افتتحتها facebook، وتدعم أيضًا العمليات المتسلسلة، وفي معظم الحالات لا تحتاج إلى كتابة SQL يدويًا، وفلسفتها التصميمية تعتمد على الرسوم البيانية (كما في بنية البيانات)، وتطبيقها يعتمد على توليد الكود بدلاً من الانعكاس (وهو ما أتفق معه)، لكن الوثائق بالإنجليزية بالكامل، وهناك عتبة معينة للبدء.

التثبيت

تثبيت مكتبة gorm

sh
$ go get -u gorm.io/gorm

الاتصال

gorm يدعم حاليًا قواعد البيانات التالية

  • MySQL: "gorm.io/driver/mysql"
  • PostgreSQL: "gorm.io/driver/postgres"
  • SQLite: "gorm.io/driver/sqlite"
  • SQL Server: "gorm.io/driver/sqlserver"
  • TIDB: "gorm.io/driver/mysql"، TIDB متوافق مع بروتوكول MySQL
  • ClickHouse: "gorm.io/driver/clickhouse"

بالإضافة إلى ذلك، هناك بعض برامج تشغيل قواعد البيانات الأخرى التي يوفرها مطورون طرف ثالث، مثل برنامج تشغيل Oracle CengSin/oracle. ستستخدم هذه المقالة MySQL للتوضيح، وأي قاعدة بيانات تستخدمها تحتاج إلى تثبيت برنامج التشغيل المناسب. هنا سنثبت برنامج تشغيل MySQL لـ gorm.

sh
$ go get -u gorm.io/driver/mysql

ثم استخدم dsn (اسم مصدر البيانات) للاتصال بقاعدة البيانات، ومكتبة برنامج التشغيل ستقوم تلقائيًا بتحليل dsn إلى التكوين المناسب

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  dsn := "root:123456@tcp(192.168.48.138:3306)/hello?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn))
  if err != nil {
    slog.Error("db connect error", err)
  }
  slog.Info("db connect success")
}

أو تمرير التكوين يدويًا

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
  "log/slog"
)

func main() {
  db, err := gorm.Open(mysql.New(mysql.Config{}))
  if err != nil {
    slog.Error("db connect error", err)
  }
  slog.Info("db connect success")
}

كلتا الطريقتان متكافئتان، حسب عادة استخدامك.

تكوين الاتصال

من خلال تمرير بنية تكوين gorm.Config، يمكننا التحكم في بعض سلوكيات gorm

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

فيما يلي بعض التفسيرات البسيطة، يمكنك التكوين وفقًا لاحتياجاتك.

go
type Config struct {
  // تعطيل المعاملات الافتراضية، gorm يفتح معاملة عند الإنشاء والتحديث الفردي للحفاظ على تناسق البيانات
  SkipDefaultTransaction bool
  // استراتيجية التسمية المخصصة
  NamingStrategy schema.Namer
  // حفظ جميع العلاقات
  FullSaveAssociations bool
  // مسجل مخصص
  Logger logger.Interface
  // دالة الوقت المخصصة، لحقول CreatedAt و UpdatedAt
  NowFunc func() time.Time
  // توليد SQL فقط دون تنفيذه
  DryRun bool
  // استخدام العبارات المترجمة مسبقًا
  PrepareStmt bool
  // بعد إنشاء الاتصال، ping لقاعدة البيانات
  DisableAutomaticPing bool
  // تجاهل المفاتيح الأجنبية عند ترحيل قاعدة البيانات
  DisableForeignKeyConstraintWhenMigrating bool
  // تجاهل مراجع العلاقات عند ترحيل قاعدة البيانات
  IgnoreRelationshipsWhenMigrating bool
  // تعطيل المعاملات المتداخلة
  DisableNestedTransaction bool
  // السماح بالتحديث العام، أي UPDATE بدون WHERE
  AllowGlobalUpdate bool
  // الاستعلام عن جميع حقول الجدول
  QueryFields bool
  // حجم الإنشاء الدفعي
  CreateBatchSize int
  // تفعيل تحويل الأخطاء
  TranslateError bool

  // ClauseBuilders منشئ الجمل
  ClauseBuilders map[string]clause.ClauseBuilder
  // ConnPool مجمع اتصالات قاعدة البيانات
  ConnPool ConnPool
  // Dialector لهجة قاعدة البيانات
  Dialector
  // Plugins الإضافات المسجلة
  Plugins map[string]Plugin

  callbacks  *callbacks
  cacheStore *sync.Map
}

النماذج

في gorm، النموذج يتوافق مع جدول قاعدة البيانات، وعادة ما يُعرض كبنية، مثل البنية التالية.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string
}

الجزء الداخلي للبنية يمكن أن يتكون من أنواع البيانات الأساسية والأنواع التي تطبق واجهتي sql.Scanner و sql.Valuer. بشكل افتراضي، البنية Person ترتبط بجدول اسمه persons، بأسلوب الجمع الحوذي، مع فصل بشرطة سفلية. أسماء الأعمدة أيضًا بأسلوب حوذي، مثل Id يطابق اسم العمود id، و gorm يوفر أيضًا طرقًا للتكوين.

تحديد اسم العمود

من خلال وسوم البنية، يمكن تحديد اسم العمود لحقل البنية، وهكذا عند تعيين الكيان، سيستخدم gorm اسم العمود المحدد.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

تحديد اسم الجدول

من خلال تطبيق واجهة Table، يمكن تحديد اسم الجدول، ولديها دالة واحدة فقط تعيد اسم الجدول.

go
type Tabler interface {
  TableName() string
}

في الدالة المُطبقة، تعيد السلسلة person، وعند ترحيل قاعدة البيانات، سيقوم gorm بإنشاء جدول اسمه person.

go
type Person struct {
  Id      uint   `gorm:"column:ID;"`
  Name    string `gorm:"column:Name;"`
  Address string
  Mom     string
  Dad     string
}

func (p Person) TableName() string {
  return "person"
}

بالنسبة لاستراتيجية التسمية، يمكن أيضًا تمرير تطبيق استراتيجيتك الخاصة عند إنشاء الاتصال للوصول إلى تأثير مخصص.

تتبع الوقت

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

func (p Person) TableName() string {
  return "person"
}

عند وجود حقول CreatedAt أو UpdatedAt، عند إنشاء أو تحديث سجل، إذا كانت قيمتها صفرية، سيقوم gorm تلقائيًا باستخدام time.Now() لضبط الوقت.

go
db.Create(&Person{
    Name:    "jack",
    Address: "usa",
    Mom:     "lili",
    Dad:     "tom",
  })

// INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom','2023-10-25 14:43:57.16','2023-10-25 14:43:57.16')

gorm يدعم أيضًا تتبع الطوابع الزمنية

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  // نانوثانية
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // ميلي ثانية
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

عند تنفيذ Create، يكون مكافئًا لـ SQL التالي

sql
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)

في الواقع، إذا كانت هناك حاجة لتتبع الوقت، أوصي بتخزين الطابع الزمني في الخلفية، ففي حالة المناطق الزمنية المختلفة، المعالجة تكون أبسط.

Model

gorm يوفر بنية Model مسبقة، تحتوي على المفتاح الأساسي ID، وحقلين لتتبع الوقت، وحقل للحذف الناعم.

go
type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

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

go
type Order struct {
  gorm.Model
  Name string
}

وهكذا ستكتسب تلقائيًا جميع خصائص gorm.Model.

المفتاح الأساسي

بشكل افتراضي، الحقل المسمى Id هو المفتاح الأساسي، ويمكن تحديد حقل المفتاح الأساسي باستخدام وسوم البنية

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

حقول متعددة تشكل مفتاح أساسي مركب

go
type Person struct {
  Id      uint `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
  Address string
  Mom     string
  Dad     string

  CreatedAt sql.NullTime
  UpdatedAt sql.NullTime
}

الفهارس

من خلال وسم index يمكن تحديد فهرس العمود

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique,sort:desc;"`
  Mom     string
  Dad     string

  // نانوثانية
  CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
  // ميلي ثانية
  UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

في البنية أعلاه، تم إنشاء فهرس فريد لحقل Address. حقلان يستخدمان نفس اسم الفهرس سيُنشئان فهرسًا مركبًا

go
type Person struct {
    Id      uint   `gorm:"primaryKey;"`
    Name    string `gorm:"primaryKey;"`
    Address string `gorm:"index:idx_addr,unique;"`
    School  string `gorm:"index:idx_addr,unique;"`
    Mom     string
    Dad     string

    // نانوثانية
    CreatedAt uint64 `gorm:"autoCreateTime:nano;"`
    // ميلي ثانية
    UpdatedAt uint64 `gorm:"autoUpdateTime;milli;"`
}

المفتاح الأجنبي

في البنية، يتم تعريف علاقات المفتاح الأجنبي من خلال تضمين البنى، مثل

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;"`
}

type Mom struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

type Dad struct {
  Id   uint
  Name string

  Persons []Person `gorm:"foreignKey:DadId;"`
}

في المثال، البنية Person لها مفتاحان أجنبيان، يشيران إلى المفاتيح الأساسية للبنيين Dad و Mom، والمرجع الافتراضي هو المفتاح الأساسي. Person لها علاقة واحد لواحد مع Dad و Mom، شخص واحد يمكن أن يكون له أب وأم واحد فقط. Dad و Mom لهما علاقة واحد لكثير مع Person، لأن الأب والأم يمكن أن يكون لهما عدة أطفال.

go
Mom   Mom `gorm:"foreignKey:MomId;"`

الغرض من تضمين البنية هو تسهيل تحديد المفتاح الأجنبي والمرجع، وبشكل افتراضي، اسم حقل المفتاح الأجنبي يكون بتنسيق اسم النوع المُشار إليه + Id، مثل MomId. وبشكل افتراضي يُشار إلى المفتاح الأساسي، ويمكن تحديد حقل معين للإشارة إليه من خلال وسوم البنية

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string

  MomId uint
  Mom   Mom `gorm:"foreignKey:MomId;references:Sid;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`

  DadId uint
  Dad   Dad `gorm:"foreignKey:DadId;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

type Mom struct {
  Id   uint
  Sid  uint `gorm:"uniqueIndex;"`
  Name string

  Persons []Person `gorm:"foreignKey:MomId;"`
}

حيث constraint:OnUpdate:CASCADE,OnDelete:SET NULL; هو تعريف قيود المفتاح الأجنبي.

الخطافات

نموذج كيان يمكنه تخصيص الخطافات

  • إنشاء
  • تحديث
  • حذف
  • استعلام

الواجهات المقابلة هي

go
// يُطلق قبل الإنشاء
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}

// يُطلق بعد الإنشاء
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}

// يُطلق قبل التحديث
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}

// يُطلق بعد التحديث
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}

// يُطلق قبل الحفظ
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}

// يُطلق بعد الحفظ
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}

// يُطلق قبل الحذف
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}

// يُطلق بعد الحذف
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}

// يُطلق بعد الاستعلام
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

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

الوسوم

فيما يلي بعض الوسوم التي يدعمها gorm

اسم الوسمالوصف
columnتحديد اسم عمود قاعدة البيانات
typeنوع بيانات العمود، يُوصى باستخدام أنواع عامة متوافقة جيدًا، مثل: bool، int، uint، float، string، time، bytes المدعومة من جميع قواعد البيانات ويمكن استخدامها مع وسوم أخرى، مثل: not null، size، autoIncrement... كما يُدعم تحديد نوع بيانات قاعدة البيانات مثل varbinary(8)
serializerتحديد المُسلسل لتسلسل أو إلغاء تسلسل البيانات إلى قاعدة البيانات، مثل: serializer:json/gob/unixtime
sizeتحديد حجم أو طول نوع بيانات العمود، مثل size: 256
primaryKeyتعريف العمود كمفتاح أساسي
uniqueتعريف العمود كمفتاح فريد
defaultتحديد القيمة الافتراضية للعمود
precisionتحديد دقة العمود
scaleتحديد حجم العمود
not nullتحديد العمود كـ NOT NULL
autoIncrementتحديد العمود كتزايد تلقائي
autoIncrementIncrementخطوة التزايد التلقائي، تتحكم في الفاصل بين السجلات المتتالية
embeddedحقل متداخل
embeddedPrefixبادئة اسم عمود الحقل المتداخل
autoCreateTimeتتبع الوقت الحالي عند الإنشاء، لحقل int يتتبع ثواني الطابع الزمني، يمكنك استخدام nano/milli لتتبع نانو ثانية، ميلي ثانية، مثل: autoCreateTime:nano
autoUpdateTimeتتبع الوقت الحالي عند الإنشاء/التحديث، لحقل int يتتبع ثواني الطابع الزمني، يمكنك استخدام nano/milli لتتبع نانو ثانية، ميلي ثانية، مثل: autoUpdateTime:milli
indexإنشاء فهرس وفقًا للمعاملات، حقول متعددة تستخدم نفس الاسم تُنشئ فهرسًا مركبًا
uniqueIndexمثل index، لكن يُنشئ فهرسًا فريدًا
checkإنشاء قيد فحص، مثل check:age > 13
<-ضبط صلاحية كتابة الحقل، <-:create إنشاء فقط، <-:update تحديث فقط، <-:false بدون صلاحية كتابة، <- صلاحية إنشاء وتحديث
->ضبط صلاحية قراءة الحقل، ->:false بدون صلاحية قراءة
-تجاهل الحقل، - يعني بدون قراءة/كتابة، -:migration بدون صلاحية ترحيل، -:all بدون صلاحية قراءة/كتابة/ترحيل
commentإضافة تعليق للحقل عند الترحيل
foreignKeyتحديد عمود النموذج الحالي كمفتاح أجنبي لجدول الربط
referencesتحديد اسم عمود الجدول المُشار إليه، الذي سيُرسم كمفتاح أجنبي لجدول الربط
polymorphicتحديد نوع متعدد الأشكال، مثل اسم النموذج
polymorphicValueتحديد قيمة متعددة الأشكال، اسم الجدول الافتراضي
many2manyتحديد اسم جدول الربط
joinForeignKeyتحديد اسم عمود المفتاح الأجنبي في جدول الربط، الذي سيُرسم إلى الجدول الحالي
joinReferencesتحديد اسم عمود المفتاح الأجنبي في جدول الربط، الذي سيُرسم إلى الجدول المُشار إليه
constraintقيود العلاقة، مثل: OnUpdate، OnDelete

الترحيل

طريقة AutoMigrate ستساعدنا في الترحيل التلقائي، ستقوم بإنشاء الجداول، والقيود، والفهارس، والمفاتيح الأجنبية، إلخ.

go
func (db *DB) AutoMigrate(dst ...interface{}) error

مثال

go
type Person struct {
  Id      uint   `gorm:"primaryKey;"`
  Name    string `gorm:"type:varchar(100);uniqueIndex;"`
  Address string
}

type Order struct {
  Id   uint
  Name string
}

db.AutoMigrate(Person{}, Order{})
// CREATE TABLE `person` (`id` bigint unsigned AUTO_INCREMENT,`name` varchar(100),`address` longtext,PRIMARY KEY (`id`),UNIQUE INDEX `idx_person_name` (`name`))
// CREATE TABLE `orders` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,PRIMARY KEY (`id`))

أو يمكننا العمل يدويًا، من خلال طريقة Migrator للوصول إلى واجهة Migrator

go
func (db *DB) Migrator() Migrator

تدعم الواجهة الطرق التالية

go
type Migrator interface {
  // AutoMigrate
  AutoMigrate(dst ...interface{}) error

  // Database
  CurrentDatabase() string
  FullDataTypeOf(*schema.Field) clause.Expr
  GetTypeAliases(databaseTypeName string) []string

  // Tables
  CreateTable(dst ...interface{}) error
  DropTable(dst ...interface{}) error
  HasTable(dst interface{}) bool
  RenameTable(oldName, newName interface{}) error
  GetTables() (tableList []string, err error)
  TableType(dst interface{}) (TableType, error)

  // Columns
  AddColumn(dst interface{}, field string) error
  DropColumn(dst interface{}, field string) error
  AlterColumn(dst interface{}, field string) error
  MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
  HasColumn(dst interface{}, field string) bool
  RenameColumn(dst interface{}, oldName, field string) error
  ColumnTypes(dst interface{}) ([]ColumnType, error)

  // Views
  CreateView(name string, option ViewOption) error
  DropView(name string) error

  // Constraints
  CreateConstraint(dst interface{}, name string) error
  DropConstraint(dst interface{}, name string) error
  HasConstraint(dst interface{}, name string) bool

  // Indexes
  CreateIndex(dst interface{}, name string) error
  DropIndex(dst interface{}, name string) error
  HasIndex(dst interface{}, name string) bool
  RenameIndex(dst interface{}, oldName, newName string) error
  GetIndexes(dst interface{}) ([]Index, error)
}

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

تحديد تعليق الجدول

عند الترحيل، إذا كنت تريد إضافة تعليق للجدول، يمكنك ضبطه بالطريقة التالية

go
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})

لاحظ أنه إذا كنت تستخدم طريقة AutoMigrate() للترحيل، وكانت هناك علاقات مرجعية بين البنى، سيقوم gorm بالترحيل التكراري وإنشاء الجداول المُشار إليها أولاً، مما يؤدي إلى تكرار تعليقات الجداول المُشار إليها والجداول المرجعية، لذا يُوصى باستخدام طريقة CreateTable للإنشاء.

TIP

عند إنشاء الجدول، طريقة CreateTable تحتاج إلى ضمان إنشاء الجداول المُشار إليها قبل الجداول المرجعية، وإلا سيحدث خطأ، بينما طريقة AutoMigrate لا تحتاج إلى ذلك لأنها ستتبع علاقات المرجعية وتُنشئ تكراريًا.

الإنشاء

Create

عند إنشاء سجل جديد، معظم الحالات ستستخدم طريقة Create

go
func (db *DB) Create(value interface{}) (tx *DB)

البنية التالية موجودة

go
type Person struct {
  Id   uint `gorm:"primaryKey;"`
  Name string
}

إنشاء سجل واحد

go
user := Person{
    Name: "jack",
}

// يجب تمرير المرجع
db = db.Create(&user)

// الخطأ الذي حدث أثناء التنفيذ
err = db.Error
// عدد السجلات المُنشأة
affected := db.RowsAffected

بعد اكتمال الإنشاء، سيقوم gorm بكتابة المفتاح الأساسي في بنية user، ولهذا يجب تمرير المؤشر. إذا تم تمرير شريحة، سيتم إنشاء دفعي

go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Create(&user)

وبالمثل، سيقوم gorm أيضًا بكتابة المفتاح الأساسي في الشريحة. عندما تكون كمية البيانات كبيرة، يمكن استخدام طريقة CreateInBatches للإنشاء على دفعات، لأن جملة SQL المُولدة INSERT INTO table VALUES (),() ستصبح طويلة، وكل قاعدة بيانات لها حد لطول SQL، لذا عند الضرورة يمكن اختيار الإنشاء على دفعات.

go
db = db.CreateInBatches(&user, 50)

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

go
func (db *DB) Save(value interface{}) (tx *DB)
go
user := []Person{
    {Name: "jack"},
    {Name: "mike"},
    {Name: "lili"},
}

db = db.Save(&user)

Upsert

طريقة Save يمكنها فقط مطابقة المفتاح الأساسي، يمكننا بناء Clause لإنجاز upsert أكثر تخصيصًا. مثل السطر التالي

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: false,
    DoUpdates: clause.AssignmentColumns([]string{"address"}),
    UpdateAll: false,
}).Create(&p)

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

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    DoNothing: true,
}).Create(&p)

أو تحديث جميع الحقول مباشرة

go
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{{Name: "name"}},
    UpdateAll: true,
}).Create(&p)

قبل استخدام upsert، تذكر إضافة فهرس لحقل التعارض.

الاستعلام

First

بالنسبة للاستعلام، يوفر gorm طرقًا عديدة متاحة، الأولى هي طريقة First

go
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)

وظيفتها هي البحث عن أول سجل بترتيب المفتاح الأساسي تصاعديًا، مثل

go
var person Person
result := db.First(&person)
err := result.Error
affected := result.RowsAffected

تمرير مؤشر dest لتسهيل تعيين البيانات المستعلم عنها إلى البنية.

أو استخدام طريقتي Table و Model لتحديد جدول الاستعلام، الأولى تقبل اسم الجدول كسلسلة، والثانية تقبل نموذج الكيان.

db.Table("person").Find(&p)
db.Model(Person{}).Find(&p)

TIP

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

Take

طريقة Take مشابهة لـ First، الفرق أنها لا ترتب حسب المفتاح الأساسي.

go
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)
go
var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffected

Pluck

طريقة Pluck تُستخدم للاستعلام الدفعي عن عمود واحد من جدول، ويمكن جمع نتائج الاستعلام في شريحة من نوع محدد، وليس بالضرورة أن تكون شريحة من نوع الكيان.

go
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)

مثل جمع عناوين جميع الأشخاص في شريحة سلاسل

go
var adds []string

// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)

وهو مكافئ لـ

go
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)

Count

طريقة Count تُستخدم لحساب عدد سجلات الكيان

go
func (db *DB) Count(count *int64) (tx *DB)

مثال على الاستخدام

go
var count int64

// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)

Find

الاستعلام الدفعي الأكثر استخدامًا هو طريقة Find

go
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)

ستبحث عن جميع السجلات المطابقة وفقًا للشروط المُعطاة

go
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)

Select

gorm بشكل افتراضي يستعلم عن جميع الحقول، يمكننا تحديد الحقول من خلال طريقة Select

go
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)

مثل

go
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)

مكافئ لـ

go
db.Select([]string{"address", "name"}).First(&p)

كما يمكن استخدام طريقة Omit لتجاهل الحقول

go
func (db *DB) Omit(columns ...string) (tx *DB)

مثل

go
// SELECT `person`.`id`,`person`.`name` FROM `person` WHERE id IN (1,2,3,4)
db.Omit("address").Where("id IN ?", []int{1, 2, 3, 4}).Find(&ps)``

الحقول المختارة أو المتجاهلة من قبل Select و Omit ستعمل في عمليات الإنشاء والتحديث والاستعلام.

Where

الاستعلام الشرطي سيستخدم طريقة Where

go
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)

مثال بسيط

go
var p Person

db.Where("id = ?", 1).First(&p)

استخدام عدة Where في العمليات المتسلسلة سيبني عدة جمل AND، مثل

go
// SELECT * FROM `person` WHERE id = 1 AND name = 'jack' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).Where("name = ?", "jack").First(&p)

أو استخدام طريقة Or لبناء جمل OR

go
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Where("address = ?", "usa").
    First(&p)

وهناك أيضًا طريقة Not، وهي مشابهة

go
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)
go
// SELECT * FROM `person` WHERE id = 1 OR name = 'jack' AND NOT name = 'mike' AND address = 'usa' ORDER BY `person`.`id` LIMIT 1
db.Where("id = ?", 1).
    Or("name = ?", "jack").
    Not("name = ?", "mike").
    Where("address = ?", "usa").
    First(&p)

بالنسبة لشرط IN، يمكن تمرير شريحة مباشرة في طريقة Where.

go
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)

أو شرط IN متعدد الأعمدة، يحتاج استخدام نوع [][]any لحمل المعاملات

go
// SELECT * FROM `person` WHERE (id, name, address) IN ((1,'jack','uk'),(2,'mike','usa'))
db.Where("(id, name, address) IN ?", [][]any{{1, "jack", "uk"}, {2, "mike", "usa"}}).Find(&ps)

gorm يدعم تجميع where، أي الجمع بين العبارات السابقة

go
db.Where(
    db.Where("name IN ?", []string{"cn", "uk"}).Where("id IN ?", []uint{1, 2}),
  ).Or(
    db.Where("name IN ?", []string{"usa", "jp"}).Where("id IN ?", []uint{3, 4}),
  ).Find(&ps)
// SELECT * FROM `person` WHERE (name IN ('cn','uk') AND id IN (1,2)) OR (name IN ('usa','jp') AND id IN (3,4))

Order

الترتيب سيستخدم طريقة Order

go
func (db *DB) Order(value interface{}) (tx *DB)

مثال على الاستخدام

go
var ps []Person

// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)

يمكن أيضًا استدعاؤها عدة مرات

go
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)

Limit

طريقتا Limit و Offset تُستخدمان غالبًا للاستعلام المقسم إلى صفحات

go
func (db *DB) Limit(limit int) (tx *DB)

func (db *DB) Offset(offset int) (tx *DB)

مثال بسيط على تقسيم الصفحات

go
var (
    ps   []Person
    page = 2
    size = 10
)

// SELECT * FROM `person` LIMIT 10 OFFSET 10
db.Offset((page - 1) * size).Limit(size).Find(&ps)

Group

طريقتا Group و Having تُستخدمان غالبًا لعمليات التجميع

go
func (db *DB) Group(name string) (tx *DB)

func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)

مثال

go
var (
    ps []Person
)

// SELECT `address` FROM `person` GROUP BY `address` HAVING address IN ('cn','us')
db.Select("address").Group("address").Having("address IN ?", []string{"cn", "us"}).Find(&ps)

Distinct

طريقة Distinct تُستخدم غالبًا لإزالة التكرار

go
func (db *DB) Distinct(args ...interface{}) (tx *DB)

مثال

go
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)

الاستعلام الفرعي

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

go
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)

استعلام فرعي from

go
// SELECT * FROM (SELECT * FROM `person` WHERE address IN ('cn','uk')) as p
db.Table("(?) as p", db.Model(Person{}).Where("address IN ?", []string{"cn", "uk"})).Find(&ps)

الأقفال

gorm يستخدم جملة clause.Locking لتوفير دعم الأقفال

go
// SELECT * FROM `person` FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&ps)

// SELECT * FROM `person` FOR SHARE NOWAIT
db.Clauses(clause.Locking{Strength: "SHARE", Options: "NOWAIT"}).Find(&ps)

التكرار

من خلال طريقة Rows يمكن الحصول على مكرر

go
func (db *DB) Rows() (*sql.Rows, error)

من خلال تكرار المكرر، واستخدام طريقة ScanRows يمكن مسح نتيجة كل سطر إلى البنية.

go
rows, err := db.Model(Person{}).Rows()
if err != nil {
    return
}
defer rows.Close()

for rows.Next() {
    var p Person
    err := db.ScanRows(rows, &p)
    if err != nil {
        return
    }
}

التعديل

save

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

go
var p Person

db.First(&p)

p.Address = "poland"
// UPDATE `person` SET `name`='json',`address`='poland' WHERE `id` = 2
db.Save(&p)

يمكنك أن ترى أنها أضفت جميع الحقول باستثناء المفتاح الأساسي إلى جملة SET.

update

لذا في معظم الحالات، يُنصح باستخدام طريقة Update

go
func (db *DB) Update(column string, value interface{}) (tx *DB)

تُستخدم أساسًا لتحديث عمود واحد

go
var p Person

db.First(&p)

// UPDATE `person` SET `address`='poland' WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Update("address", "poland")

updates

طريقة Updates تُستخدم لتحديث عدة أعمدة، تقبل بنية وخريطة كمعاملات، وعندما تكون قيمة حقل البنية صفرية، سيتم تجاهل الحقل، لكن في الخريطة لن يُتجاهل.

go
func (db *DB) Updates(values interface{}) (tx *DB)

مثال

go
var p Person

db.First(&p)

// UPDATE `person` SET `name`='jojo',`address`='poland' WHERE `id` = 2
db.Model(p).Updates(Person{Name: "jojo", Address: "poland"})

// UPDATE `person` SET `address`='poland',`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "address": "poland"})

تعابير SQL

في بعض الأحيان، غالبًا ما تحتاج إلى عمليات زيادة ذاتية أو نقصان ذاتي أو عمليات حسابية على الحقل، عمومًا تُستعلم أولًا ثم تُحسب ثم تُحدّث، أو تُستخدم تعابير SQL.

go
func Expr(expr string, args ...interface{}) clause.Expr

مثال

go
// UPDATE `person` SET `age`=age + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age + age")})

// UPDATE `person` SET `age`=age * 2 + age,`name`='jojo' WHERE `id` = 2
db.Model(p).Updates(map[string]any{"name": "jojo", "age": gorm.Expr("age * 2 + age")})

الحذف

في gorm، حذف السجلات سيستخدم طريقة Delete، يمكن تمرير بنية الكيان مباشرة، أو تمرير الشرط.

go
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)

مثل تمرير البنية مباشرة

go
var p Person

db.First(&p)

// // DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)

أو

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)

أو تحديد الشرط

go
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)

يمكن أيضًا كتابتها مختصرة

go
var p Person

db.First(&p)

// DELETE FROM `person` WHERE id = 2
db.Delete(&Person{}, "id = ?", 2)

// DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&Person{}, 2)

للحذف الدفعي، مرّر شريحة

go
// DELETE FROM `person` WHERE id IN (1,2,3)
db.Delete(&Person{}, "id IN ?", []uint{1, 2, 3})
// DELETE FROM `person` WHERE `person`.`id` IN (1,2,3)
db.Delete(&Person{}, []uint{1, 2, 3})

الحذف الناعم

إذا كان نموذج الكيان يستخدم الحذف الناعم، فعند الحذف، بشكل افتراضي يتم التحديث، وللحذف نهائيًا يمكن استخدام طريقة Unscope

go
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})

تعريف العلاقات

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

واحد لواحد

علاقة واحد لواحد هي الأبسط، في الحالة الطبيعية شخص واحد يمكن أن يكون له أم واحدة فقط، انظر البنية التالية

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`
}

type Mom struct {
  Id   uint
  Name string
}

البنية Person من خلال تضمين البنية Mom، حققت الإشارة إلى نوع Mom، حيث Person.MomId هو حقل الإشارة، والمفتاح الأساسي Mom.Id هو الحقل المُشار إليه، وهكذا اكتملت علاقة واحد لواحد. كيفية تخصيص المفتاح الأجنبي والإشارة والقيود والقواعد الافتراضية للمفتاح الأجنبي تم شرحها في تعريف المفتاح الأجنبي.

TIP

لحقول المفتاح الأجنبي، يُوصى باستخدام الأنواع التي يوفرها حزمة sql، لأن المفتاح الأجنبي بشكل افتراضي يمكن أن يكون NULL، وعند استخدام Create لإنشاء سجل، إذا استُخدم نوع عادي، القيمة الصفرية 0 ستُنشأ أيضًا، والمفتاح الأجنبي غير الموجود لا يُسمح بإنشائه بوضوح.

واحد لكثير

أضف بنية مدرسة، المدرسة والطلاب في علاقة واحد لكثير، مدرسة واحدة بها عدة طلاب، لكن طالب واحد يمكنه الدراسة في مدرسة واحدة فقط.

go
type Person struct {
    Id      uint
    Name    string
    Address string
    Age     uint

    MomId sql.NullInt64
    Mom   Mom `gorm:"foreignKey:MomId;"`

    SchoolId sql.NullInt64
    School   School `gorm:"foreignKey:SchoolId;"`
}

type Mom struct {
    Id   uint
    Name string
}


type School struct {
    Id   uint
    Name string

    Persons []Person `gorm:"foreignKey:SchoolId;"`
}

school.Persons من نوع []person، يعني أنه يمكن أن يمتلك عدة طلاب، و Person يجب أن يحتوي على مفتاح أجنبي يشير إلى School، وهو Person.SchoolId.

كثير لكثير

شخص واحد يمكن أن يمتلك عدة منازل، ومنزل واحد يمكن أن يسكنه عدة أشخاص، هذه علاقة كثير لكثير.

go
type Person struct {
  Id      uint
  Name    string
  Address string
  Age     uint

  MomId sql.NullInt64
  Mom   Mom `gorm:"foreignKey:MomId;"`

  SchoolId sql.NullInt64
  School   School `gorm:"foreignKey:SchoolId;"`

  Houses []House `gorm:"many2many:person_house;"`
}

type Mom struct {
  Id   uint
  Name string
}

type School struct {
  Id   uint
  Name string

  Persons []Person
}

type House struct {
  Id   uint
  Name string

  Persons []Person `gorm:"many2many:person_house;"`
}

type PersonHouse struct {
  PersonId sql.NullInt64
  Person   Person `gorm:"foreignKey:PersonId;"`
  HouseId  sql.NullInt64
  House    House `gorm:"foreignKey:HouseId;"`
}

Person و House يحملان نوع شريحة للآخر مما يدل على علاقة كثير لكثير، علاقة كثير لكثير تحتاج عمومًا إلى إنشاء جدول ربط، من خلال many2many لتحديد جدول الربط، ومفتاحا جدول الربط يجب تحديدهما بشكل صحيح.

بعد إنشاء البنى، دع gorm يرحّلها تلقائيًا إلى قاعدة البيانات

go
tables := []any{
    School{},
    Mom{},
    Person{},
    House{},
    PersonHouse{},
}
for _, table := range tables {
    db.Migrator().CreateTable(&table)
}

لاحظ ترتيب إنشاء الجداول المرجعية والمُشار إليها.

عمليات العلاقات

بعد إنشاء العلاقات الثلاث السابقة، الآن كيفية استخدام العلاقات للإنشاء والحذف والتحديث والاستعلام. هذا سيستخدم بشكل رئيسي طريقة Association

go
func (db *DB) Association(column string) *Association

تقبل معامل علاقة، وقيمته يجب أن تكون اسم حقل النوع المُشار إليه في البنية المُدرجة.

go
db.Model(&person).Association("Mom").Find(&mom)

مثل البحث عن علاقة أم الشخص، معامل Association هو Mom، أي اسم الحقل Person.Mom.

إنشاء العلاقات

go
// تعريف البيانات
jenny := Mom{
    Name: "jenny",
}

mit := School{
    Name:    "MIT",
    Persons: nil,
}

h1 := House{
    Id:      0,
    Name:    "h1",
    Persons: nil,
}

h2 := House{
    Name:    "h2",
    Persons: nil,
}

jack := Person{
    Name:    "jack",
    Address: "usa",
    Age:     18,
}

mike := Person{
    Name:    "mike",
    Address: "uk",
    Age:     20,
}

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('jack','usa',18,NULL,NULL)
db.Create(&jack)
// INSERT INTO `schools` (`name`) VALUES ('MIT')
db.Create(&mit)

// إضافة علاقة Person و Mom، علاقة واحد لواحد
// INSERT INTO `moms` (`name`) VALUES ('jenny') ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=1 WHERE `id` = 1
db.Model(&jack).Association("Mom").Append(&jenny)

// إضافة علاقة school و Person، علاقة واحد لكثير
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('jack','usa',18,1,1,1),('mike','uk',20,NULL,1,DEFAULT) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
db.Model(&mit).Association("Persons").Append([]Person{jack, mike})

// إضافة علاقة Person و Houses، علاقة كثير لكثير
// INSERT INTO `houses` (`name`) VALUES ('h1'),('h2') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,1),(1,2) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
db.Model(&jack).Association("Houses").Append([]House{h1, h2})

إذا كانت جميع السجلات غير موجودة، عند إنشاء العلاقة، سيقوم بإنشاء السجل أولًا ثم إنشاء العلاقة.

البحث عن العلاقات

فيما يلي كيفية البحث عن العلاقات.

go
// بحث علاقة واحد لواحد
var person Person
var mom Mom

// SELECT * FROM `people` ORDER BY `people`.`id` LIMIT 1
db.First(&person)
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
db.Model(person).Association("Mom").Find(&mom)

// بحث علاقة واحد لكثير
var school School
var persons []Person

// SELECT * FROM `schools` ORDER BY `schools`.`id` LIMIT 1
db.First(&school)
// SELECT * FROM `people` WHERE `people`.`school_id` = 1
db.Model(&school).Association("Persons").Find(&persons)

// بحث علاقة كثير لكثير
var houses []House

// SELECT `houses`.`id`,`houses`.`name` FROM `houses` JOIN `person_house` ON `person_house`.`house_id` = `houses`.`id` AND `person_house`.`person_id` IN (1,2)
db.Model(&persons).Association("Houses").Find(&houses)

البحث عن العلاقات سيبحث عن السجلات المطابقة في الجدول المُشار إليه وفقًا للبيانات الموجودة، وبالنسبة لعلاقة كثير لكثير، سيقوم gorm تلقائيًا بإكمال عملية ربط الجداول.

تحديث العلاقات

فيما يلي كيفية تحديث العلاقات

go
// تحديث علاقة واحد لواحد
var jack Person

lili := Mom{
    Name: "lili",
}

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

// INSERT INTO `moms` (`name`) VALUES ('lili')
db.Create(&lili)

// INSERT INTO `moms` (`name`,`id`) VALUES ('lili',2) ON DUPLICATE KEY UPDATE `id`=`id`
// UPDATE `people` SET `mom_id`=2 WHERE `id` = 1
db.Model(&jack).Association("Mom").Replace(&lili)

// تحديث علاقة واحد لكثير

var mit School
newPerson := []Person{{Name: "bob"}, {Name: "jojo"}}
// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`) VALUES ('bob','',0,NULL,NULL),('jojo','',0,NULL,NULL)
db.Create(&newPerson)

//  SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)

// INSERT INTO `people` (`name`,`address`,`age`,`mom_id`,`school_id`,`id`) VALUES ('bob','',0,NULL,1,4),('jojo','',0,NULL,1,5) ON DUPLICATE KEY UPDATE `school_id`=VALUES(`school_id`)
//  UPDATE `people` SET `school_id`=NULL WHERE `people`.`id` NOT IN (4,5) AND `people`.`school_id` = 1
db.Model(&mit).Association("Persons").Replace(newPerson)

// تحديث علاقة كثير لكثير

// INSERT INTO `houses` (`name`) VALUES ('h3'),('h4'),('h5') ON DUPLICATE KEY UPDATE `id`=`id`
// INSERT INTO `person_house` (`person_id`,`house_id`) VALUES (1,3),(1,4),(1,5) ON DUPLICATE KEY UPDATE `person_id`=`person_id`
// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` NOT IN (3,4,5)
db.Model(&jack).Association("Houses").Replace([]House{{Name: "h3"}, {Name: "h4"}, {Name: "h5"}})

عند تحديث العلاقات، إذا كانت البيانات المُشار إليها والبيانات المرجعية غير موجودة، سيحاول gorm إنشاءها.

حذف العلاقات

فيما يلي كيفية حذف العلاقات

go
// حذف علاقة واحد لواحد
var (
    jack Person
    lili Mom
)

// SELECT * FROM `people` WHERE name = 'jack' ORDER BY `people`.`id` LIMIT 1
db.Where("name = ?", "jack").First(&jack)

//  SELECT * FROM `moms` WHERE name = 'lili' ORDER BY `moms`.`id` LIMIT 1
db.Where("name = ?", "lili").First(&lili)

// UPDATE `people` SET `mom_id`=NULL WHERE `people`.`id` = 1 AND `people`.`mom_id` = 2
db.Model(&jack).Association("Mom").Delete(&lili)

// حذف علاقة واحد لكثير

var (
    mit     School
    persons []Person
)

// SELECT * FROM `schools` WHERE name = 'mit' ORDER BY `schools`.`id` LIMIT 1
db.Where("name = ?", "mit").First(&mit)
// SELECT * FROM `people` WHERE name IN ('jack','mike')
db.Where("name IN ?", []string{"jack", "mike"}).Find(&persons)

// UPDATE `people` SET `school_id`=NULL WHERE `people`.`school_id` = 1 AND `people`.`id` IN (1,2)
db.Model(&mit).Association("Persons").Delete(&persons)

// حذف علاقة كثير لكثير
var houses []House

// SELECT * FROM `houses` WHERE name IN ('h3','h4')
db.Where("name IN ?", []string{"h3", "h4"}).Find(&houses)

// DELETE FROM `person_house` WHERE `person_house`.`person_id` = 1 AND `person_house`.`house_id` IN (3,4)
db.Model(&jack).Association("Houses").Delete(&houses)

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

go
db.Model(&jack).Association("Houses").Clear()

إذا كنت تريد حذف سجلات الكيان المقابلة، يمكنك إضافة عملية Unscoped بعد عملية Association (لن تؤثر على many2many)

go
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)

بالنسبة لعلاقة واحد لكثير وكثير لكثير، يمكن استخدام عملية Select لحذف السجلات

go
var (
    mit     School
)
db.Where("name = ?", "mit").First(&mit)

db.Select("Persons").Delete(&mit)

التحميل المسبق

يُستخدم التحميل المسبق للاستعلام عن بيانات العلاقات، بالنسبة للكيانات ذات العلاقات، سيقوم أولاً بتحميل الكيانات المُشار إليها مسبقًا. استعلام العلاقات المذكور سابقًا هو استعلام عن العلاقات، والتحميل المسبق هو استعلام مباشر عن سجلات الكيان، بما في ذلك جميع العلاقات. من الناحية النحوية، استعلام العلاقات يحتاج أولًا للاستعلام عن []Person المحددة، ثم الاستعلام عن []Mom المرتبطة، والتحميل المسبق نحويًا يستعلم مباشرة عن []Person، ويحمل أيضًا جميع العلاقات، لكن في الواقع SQL المنفذة متشابهة تقريبًا. مثال

go
var users []Person

// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)

هذا مثال على استعلام علاقة واحد لواحد، والمخرجات

go
[{Id:1 Name:jack Address:usa Age:18 MomId:{Int64:1 Valid:true} Mom:{Id:1 Name:jenny} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]} {Id:2 Name:mike Address:uk Age:20 MomId:{Int64:0 Valid:false} Mom:{Id:0 Name:} SchoolId:{Int64:1 Valid:true} School:{Id:0 Name: Persons:[]} Houses:[]}]

يمكنك أن ترى أن Mom المرتبطة تم استعلامها أيضًا، لكن لم يتم تحميل علاقة المدرسة مسبقًا، لذا جميع بنى School قيم صفرية. يمكن أيضًا استخدام clause.Associations للإشارة إلى التحميل المسبق لجميع العلاقات، باستثناء العلاقات المتداخلة.

go
db.Preload(clause.Associations).Find(&users)

مثال على التحميل المسبق المتداخل، وظيفته الاستعلام عن جميع الطلاب المرتبطين بكل مدرسة وأم كل طالب ومنازل كل طالب، وأيضًا مالكو كل منزل، المدرسة -> الطالب -> المنزل -> الطالب.

go
var schools []School

db.Preload("Persons").
    Preload("Persons.Mom").
    Preload("Persons.Houses").
    Preload("Persons.Houses.Persons").Find(&schools)

// كود المخرجات، المنطق يمكن تجاهله
for _, school := range schools {
    fmt.Println("school", school.Name)
    for _, person := range school.Persons {
        fmt.Println("person", person.Name)
        fmt.Println("mom", person.Mom.Name)
        for _, house := range person.Houses {
            var persons []string
            for _, p := range house.Persons {
                persons = append(persons, p.Name)
            }
            fmt.Println("house", house.Name, "owner", persons)
        }
        fmt.Println()
    }
}

المخرجات

school MIT
person jack
mom jenny
house h1 owner [jack]
house h2 owner [jack]

person mike
mom

يمكنك أن ترى أنه تم إخراج أم كل طالب في كل مدرسة ومنازلهم، ومالكي كل منزل.

المعاملات

gorm يفتح المعاملات افتراضيًا، أي عملية إدراج أو تحديث فاشلة سيتم التراجع عنها، يمكن تعطيلها في تكوين الاتصال، وسيزيد الأداء بحوالي 30%. استخدام المعاملات في gorm له عدة طرق، فيما يلي مقدمة بسيطة.

تلقائي

معاملة الإغلاق، من خلال طريقة Transaction، بتمرير دالة إغلاق، إذا كانت القيمة المرجعة للدالة ليست nil، سيتم التراجع تلقائيًا.

go
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)

مثال، العمليات في الإغلاق يجب أن تستخدم المعامل tx، وليس db الخارجي.

go
var ps []Person

db.Transaction(func(tx *gorm.DB) error {
    err := tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Create(&ps).Error
    if err != nil {
        return err
    }

    err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
    if err != nil {
        return err
    }

    return nil
})

يدوي

يُنصح باستخدام المعاملات اليدوية، حيث نتحكم نحن في متى نتراجع ومتى نُرسل. المعاملات اليدوية ستستخدم الطرق الثلاث التالية

go
// طريقة Begin لفتح المعاملة
func (db *DB) Begin(opts ...*sql.TxOptions) *DB

// طريقة Rollback للتراجع عن المعاملة
func (db *DB) Rollback() *DB

// طريقة Commit لإرسال المعاملة
func (db *DB) Commit() *DB

مثال، بعد فتح المعاملة، يجب استخدام tx للعمل على ORM.

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.Rollback()
    return
}

tx.Commit()

يمكن تحديد نقطة تراجع

go
var ps []Person

tx := db.Begin()

err := tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

tx.SavePoint("createBatch")

err = tx.Create(&ps).Error
if err != nil {
    tx.Rollback()
    return
}

err = tx.Model(Person{}).Where("id = ?", 1).Update("name", "jack").Error
if err != nil {
    tx.RollbackTo("createBatch")
    return
}

tx.Commit()

الخلاصة

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

Golang تم تحريره بواسطة www.golangdev.cn