مكتبة 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
$ 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.
$ go get -u gorm.io/driver/mysqlثم استخدم dsn (اسم مصدر البيانات) للاتصال بقاعدة البيانات، ومكتبة برنامج التشغيل ستقوم تلقائيًا بتحليل dsn إلى التكوين المناسب
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")
}أو تمرير التكوين يدويًا
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
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})فيما يلي بعض التفسيرات البسيطة، يمكنك التكوين وفقًا لاحتياجاتك.
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، النموذج يتوافق مع جدول قاعدة البيانات، وعادة ما يُعرض كبنية، مثل البنية التالية.
type Person struct {
Id uint
Name string
Address string
Mom string
Dad string
}الجزء الداخلي للبنية يمكن أن يتكون من أنواع البيانات الأساسية والأنواع التي تطبق واجهتي sql.Scanner و sql.Valuer. بشكل افتراضي، البنية Person ترتبط بجدول اسمه persons، بأسلوب الجمع الحوذي، مع فصل بشرطة سفلية. أسماء الأعمدة أيضًا بأسلوب حوذي، مثل Id يطابق اسم العمود id، و gorm يوفر أيضًا طرقًا للتكوين.
تحديد اسم العمود
من خلال وسوم البنية، يمكن تحديد اسم العمود لحقل البنية، وهكذا عند تعيين الكيان، سيستخدم gorm اسم العمود المحدد.
type Person struct {
Id uint `gorm:"column:ID;"`
Name string `gorm:"column:Name;"`
Address string
Mom string
Dad string
}تحديد اسم الجدول
من خلال تطبيق واجهة Table، يمكن تحديد اسم الجدول، ولديها دالة واحدة فقط تعيد اسم الجدول.
type Tabler interface {
TableName() string
}في الدالة المُطبقة، تعيد السلسلة person، وعند ترحيل قاعدة البيانات، سيقوم gorm بإنشاء جدول اسمه person.
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"
}بالنسبة لاستراتيجية التسمية، يمكن أيضًا تمرير تطبيق استراتيجيتك الخاصة عند إنشاء الاتصال للوصول إلى تأثير مخصص.
تتبع الوقت
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() لضبط الوقت.
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 يدعم أيضًا تتبع الطوابع الزمنية
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 التالي
INSERT INTO `person` (`name`,`address`,`mom`,`dad`,`created_at`,`updated_at`) VALUES ('jack','usa','lili','tom',1698216540519000000,1698216540)في الواقع، إذا كانت هناك حاجة لتتبع الوقت، أوصي بتخزين الطابع الزمني في الخلفية، ففي حالة المناطق الزمنية المختلفة، المعالجة تكون أبسط.
Model
gorm يوفر بنية Model مسبقة، تحتوي على المفتاح الأساسي ID، وحقلين لتتبع الوقت، وحقل للحذف الناعم.
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}عند الاستخدام، فقط قم بتضمينها في نموذج الكيان الخاص بك.
type Order struct {
gorm.Model
Name string
}وهكذا ستكتسب تلقائيًا جميع خصائص gorm.Model.
المفتاح الأساسي
بشكل افتراضي، الحقل المسمى Id هو المفتاح الأساسي، ويمكن تحديد حقل المفتاح الأساسي باستخدام وسوم البنية
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}حقول متعددة تشكل مفتاح أساسي مركب
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string `gorm:"primaryKey;"`
Address string
Mom string
Dad string
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
}الفهارس
من خلال وسم index يمكن تحديد فهرس العمود
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. حقلان يستخدمان نفس اسم الفهرس سيُنشئان فهرسًا مركبًا
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;"`
}المفتاح الأجنبي
في البنية، يتم تعريف علاقات المفتاح الأجنبي من خلال تضمين البنى، مثل
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، لأن الأب والأم يمكن أن يكون لهما عدة أطفال.
Mom Mom `gorm:"foreignKey:MomId;"`الغرض من تضمين البنية هو تسهيل تحديد المفتاح الأجنبي والمرجع، وبشكل افتراضي، اسم حقل المفتاح الأجنبي يكون بتنسيق اسم النوع المُشار إليه + Id، مثل MomId. وبشكل افتراضي يُشار إلى المفتاح الأساسي، ويمكن تحديد حقل معين للإشارة إليه من خلال وسوم البنية
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; هو تعريف قيود المفتاح الأجنبي.
الخطافات
نموذج كيان يمكنه تخصيص الخطافات
- إنشاء
- تحديث
- حذف
- استعلام
الواجهات المقابلة هي
// يُطلق قبل الإنشاء
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 ستساعدنا في الترحيل التلقائي، ستقوم بإنشاء الجداول، والقيود، والفهارس، والمفاتيح الأجنبية، إلخ.
func (db *DB) AutoMigrate(dst ...interface{}) errorمثال
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
func (db *DB) Migrator() Migratorتدعم الواجهة الطرق التالية
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)
}قائمة الطرق تتضمن قاعدة البيانات، والجداول، والأعمدة، والعراض، والفهارس، والقيود في أبعاد متعددة، للمستخدمين الذين يحتاجون للتخصيص يمكنهم العمل بشكل أدق.
تحديد تعليق الجدول
عند الترحيل، إذا كنت تريد إضافة تعليق للجدول، يمكنك ضبطه بالطريقة التالية
db.Set("gorm:table_options", " comment 'person table'").Migrator().CreateTable(Person{})لاحظ أنه إذا كنت تستخدم طريقة AutoMigrate() للترحيل، وكانت هناك علاقات مرجعية بين البنى، سيقوم gorm بالترحيل التكراري وإنشاء الجداول المُشار إليها أولاً، مما يؤدي إلى تكرار تعليقات الجداول المُشار إليها والجداول المرجعية، لذا يُوصى باستخدام طريقة CreateTable للإنشاء.
TIP
عند إنشاء الجدول، طريقة CreateTable تحتاج إلى ضمان إنشاء الجداول المُشار إليها قبل الجداول المرجعية، وإلا سيحدث خطأ، بينما طريقة AutoMigrate لا تحتاج إلى ذلك لأنها ستتبع علاقات المرجعية وتُنشئ تكراريًا.
الإنشاء
Create
عند إنشاء سجل جديد، معظم الحالات ستستخدم طريقة Create
func (db *DB) Create(value interface{}) (tx *DB)البنية التالية موجودة
type Person struct {
Id uint `gorm:"primaryKey;"`
Name string
}إنشاء سجل واحد
user := Person{
Name: "jack",
}
// يجب تمرير المرجع
db = db.Create(&user)
// الخطأ الذي حدث أثناء التنفيذ
err = db.Error
// عدد السجلات المُنشأة
affected := db.RowsAffectedبعد اكتمال الإنشاء، سيقوم gorm بكتابة المفتاح الأساسي في بنية user، ولهذا يجب تمرير المؤشر. إذا تم تمرير شريحة، سيتم إنشاء دفعي
user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Create(&user)وبالمثل، سيقوم gorm أيضًا بكتابة المفتاح الأساسي في الشريحة. عندما تكون كمية البيانات كبيرة، يمكن استخدام طريقة CreateInBatches للإنشاء على دفعات، لأن جملة SQL المُولدة INSERT INTO table VALUES (),() ستصبح طويلة، وكل قاعدة بيانات لها حد لطول SQL، لذا عند الضرورة يمكن اختيار الإنشاء على دفعات.
db = db.CreateInBatches(&user, 50)بالإضافة إلى ذلك، طريقة Save يمكنها أيضًا إنشاء سجلات، ووظيفتها هي عند مطابقة المفتاح الأساسي يتم تحديث السجل، وإلا يتم الإدراج.
func (db *DB) Save(value interface{}) (tx *DB)user := []Person{
{Name: "jack"},
{Name: "mike"},
{Name: "lili"},
}
db = db.Save(&user)Upsert
طريقة Save يمكنها فقط مطابقة المفتاح الأساسي، يمكننا بناء Clause لإنجاز upsert أكثر تخصيصًا. مثل السطر التالي
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: false,
DoUpdates: clause.AssignmentColumns([]string{"address"}),
UpdateAll: false,
}).Create(&p)وظيفته هي عند تعارض حقل name، تحديث قيمة حقل address، وإذا لم يكن هناك تعارض، يتم إنشاء سجل جديد. يمكن أيضًا عدم فعل أي شيء عند التعارض
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoNothing: true,
}).Create(&p)أو تحديث جميع الحقول مباشرة
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
UpdateAll: true,
}).Create(&p)قبل استخدام upsert، تذكر إضافة فهرس لحقل التعارض.
الاستعلام
First
بالنسبة للاستعلام، يوفر gorm طرقًا عديدة متاحة، الأولى هي طريقة First
func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB)وظيفتها هي البحث عن أول سجل بترتيب المفتاح الأساسي تصاعديًا، مثل
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، الفرق أنها لا ترتب حسب المفتاح الأساسي.
func (db *DB) Take(dest interface{}, conds ...interface{}) (tx *DB)var person Person
result := db.Take(&person)
err := result.Error
affected := result.RowsAffectedPluck
طريقة Pluck تُستخدم للاستعلام الدفعي عن عمود واحد من جدول، ويمكن جمع نتائج الاستعلام في شريحة من نوع محدد، وليس بالضرورة أن تكون شريحة من نوع الكيان.
func (db *DB) Pluck(column string, dest interface{}) (tx *DB)مثل جمع عناوين جميع الأشخاص في شريحة سلاسل
var adds []string
// SELECT `address` FROM `person` WHERE name IN ('jack','lili')
db.Model(Person{}).Where("name IN ?", []string{"jack", "lili"}).Pluck("address", &adds)وهو مكافئ لـ
db.Select("address").Where("name IN ?", []string{"jack", "lili"}).Find(&adds)Count
طريقة Count تُستخدم لحساب عدد سجلات الكيان
func (db *DB) Count(count *int64) (tx *DB)مثال على الاستخدام
var count int64
// SELECT count(*) FROM `person`
db.Model(Person{}).Count(&count)Find
الاستعلام الدفعي الأكثر استخدامًا هو طريقة Find
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB)ستبحث عن جميع السجلات المطابقة وفقًا للشروط المُعطاة
// SELECT * FROM `person`
var ps []Person
db.Find(&ps)Select
gorm بشكل افتراضي يستعلم عن جميع الحقول، يمكننا تحديد الحقول من خلال طريقة Select
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB)مثل
// SELECT `address`,`name` FROM `person` ORDER BY `person`.`id` LIMIT 1
db.Select("address", "name").First(&p)مكافئ لـ
db.Select([]string{"address", "name"}).First(&p)كما يمكن استخدام طريقة Omit لتجاهل الحقول
func (db *DB) Omit(columns ...string) (tx *DB)مثل
// 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
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB)مثال بسيط
var p Person
db.Where("id = ?", 1).First(&p)استخدام عدة Where في العمليات المتسلسلة سيبني عدة جمل AND، مثل
// 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
func (db *DB) Or(query interface{}, args ...interface{}) (tx *DB)// 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، وهي مشابهة
func (db *DB) Not(query interface{}, args ...interface{}) (tx *DB)// 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.
db.Where("address IN ?", []string{"cn", "us"}).Find(&ps)أو شرط IN متعدد الأعمدة، يحتاج استخدام نوع [][]any لحمل المعاملات
// 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، أي الجمع بين العبارات السابقة
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
func (db *DB) Order(value interface{}) (tx *DB)مثال على الاستخدام
var ps []Person
// SELECT * FROM `person` ORDER BY name ASC, id DESC
db.Order("name ASC, id DESC").Find(&ps)يمكن أيضًا استدعاؤها عدة مرات
// SELECT * FROM `person` ORDER BY name ASC, id DESC,address
db.Order("name ASC, id DESC").Order("address").Find(&ps)Limit
طريقتا Limit و Offset تُستخدمان غالبًا للاستعلام المقسم إلى صفحات
func (db *DB) Limit(limit int) (tx *DB)
func (db *DB) Offset(offset int) (tx *DB)مثال بسيط على تقسيم الصفحات
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 تُستخدمان غالبًا لعمليات التجميع
func (db *DB) Group(name string) (tx *DB)
func (db *DB) Having(query interface{}, args ...interface{}) (tx *DB)مثال
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 تُستخدم غالبًا لإزالة التكرار
func (db *DB) Distinct(args ...interface{}) (tx *DB)مثال
// SELECT DISTINCT `name` FROM `person` WHERE address IN ('cn','us')
db.Where("address IN ?", []string{"cn", "us"}).Distinct("name").Find(&ps)الاستعلام الفرعي
الاستعلام الفرعي هو استعلام متداخل، مثلًا إذا كنت تريد الاستعلام عن جميع الأشخاص الذين قيمة id لديهم أكبر من المتوسط
// SELECT * FROM `person` WHERE id > (SELECT AVG(id) FROM `person`
db.Where("id > (?)", db.Model(Person{}).Select("AVG(id)")).Find(&ps)استعلام فرعي from
// 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 لتوفير دعم الأقفال
// 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 يمكن الحصول على مكرر
func (db *DB) Rows() (*sql.Rows, error)من خلال تكرار المكرر، واستخدام طريقة ScanRows يمكن مسح نتيجة كل سطر إلى البنية.
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 عند الإنشاء، ويمكن استخدامها أيضًا لتحديث السجلات، وستقوم بتحديث جميع الحقول، حتى لو كانت بعض حقول البنية قيمًا صفرية، لكن إذا لم يُطابق المفتاح الأساسي سيتم الإدراج.
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
func (db *DB) Update(column string, value interface{}) (tx *DB)تُستخدم أساسًا لتحديث عمود واحد
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 تُستخدم لتحديث عدة أعمدة، تقبل بنية وخريطة كمعاملات، وعندما تكون قيمة حقل البنية صفرية، سيتم تجاهل الحقل، لكن في الخريطة لن يُتجاهل.
func (db *DB) Updates(values interface{}) (tx *DB)مثال
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.
func Expr(expr string, args ...interface{}) clause.Exprمثال
// 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، يمكن تمرير بنية الكيان مباشرة، أو تمرير الشرط.
func (db *DB) Delete(value interface{}, conds ...interface{}) (tx *DB)مثل تمرير البنية مباشرة
var p Person
db.First(&p)
// // DELETE FROM `person` WHERE `person`.`id` = 2
db.Delete(&p)أو
var p Person
db.First(&p)
// DELETE FROM `person` WHERE `person`.`id` = 2
db.Model(p).Delete(nil)أو تحديد الشرط
// DELETE FROM `person` WHERE id = 2
db.Model(Person{}).Where("id = ?", p.Id).Delete(nil)يمكن أيضًا كتابتها مختصرة
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)للحذف الدفعي، مرّر شريحة
// 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
db.Unscoped().Delete(&Person{}, []uint{1, 2, 3})تعريف العلاقات
gorm يوفر إمكانية التفاعل مع علاقات الجداول، من خلال تضمين البنى والحقول لتعريف العلاقات بين البنى.
واحد لواحد
علاقة واحد لواحد هي الأبسط، في الحالة الطبيعية شخص واحد يمكن أن يكون له أم واحدة فقط، انظر البنية التالية
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 ستُنشأ أيضًا، والمفتاح الأجنبي غير الموجود لا يُسمح بإنشائه بوضوح.
واحد لكثير
أضف بنية مدرسة، المدرسة والطلاب في علاقة واحد لكثير، مدرسة واحدة بها عدة طلاب، لكن طالب واحد يمكنه الدراسة في مدرسة واحدة فقط.
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.
كثير لكثير
شخص واحد يمكن أن يمتلك عدة منازل، ومنزل واحد يمكن أن يسكنه عدة أشخاص، هذه علاقة كثير لكثير.
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 يرحّلها تلقائيًا إلى قاعدة البيانات
tables := []any{
School{},
Mom{},
Person{},
House{},
PersonHouse{},
}
for _, table := range tables {
db.Migrator().CreateTable(&table)
}لاحظ ترتيب إنشاء الجداول المرجعية والمُشار إليها.
عمليات العلاقات
بعد إنشاء العلاقات الثلاث السابقة، الآن كيفية استخدام العلاقات للإنشاء والحذف والتحديث والاستعلام. هذا سيستخدم بشكل رئيسي طريقة Association
func (db *DB) Association(column string) *Associationتقبل معامل علاقة، وقيمته يجب أن تكون اسم حقل النوع المُشار إليه في البنية المُدرجة.
db.Model(&person).Association("Mom").Find(&mom)مثل البحث عن علاقة أم الشخص، معامل Association هو Mom، أي اسم الحقل Person.Mom.
إنشاء العلاقات
// تعريف البيانات
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})إذا كانت جميع السجلات غير موجودة، عند إنشاء العلاقة، سيقوم بإنشاء السجل أولًا ثم إنشاء العلاقة.
البحث عن العلاقات
فيما يلي كيفية البحث عن العلاقات.
// بحث علاقة واحد لواحد
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 تلقائيًا بإكمال عملية ربط الجداول.
تحديث العلاقات
فيما يلي كيفية تحديث العلاقات
// تحديث علاقة واحد لواحد
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 إنشاءها.
حذف العلاقات
فيما يلي كيفية حذف العلاقات
// حذف علاقة واحد لواحد
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 لمسح العلاقة مباشرة
db.Model(&jack).Association("Houses").Clear()إذا كنت تريد حذف سجلات الكيان المقابلة، يمكنك إضافة عملية Unscoped بعد عملية Association (لن تؤثر على many2many)
db.Model(&jack).Association("Houses").Unscoped().Delete(&houses)بالنسبة لعلاقة واحد لكثير وكثير لكثير، يمكن استخدام عملية Select لحذف السجلات
var (
mit School
)
db.Where("name = ?", "mit").First(&mit)
db.Select("Persons").Delete(&mit)التحميل المسبق
يُستخدم التحميل المسبق للاستعلام عن بيانات العلاقات، بالنسبة للكيانات ذات العلاقات، سيقوم أولاً بتحميل الكيانات المُشار إليها مسبقًا. استعلام العلاقات المذكور سابقًا هو استعلام عن العلاقات، والتحميل المسبق هو استعلام مباشر عن سجلات الكيان، بما في ذلك جميع العلاقات. من الناحية النحوية، استعلام العلاقات يحتاج أولًا للاستعلام عن []Person المحددة، ثم الاستعلام عن []Mom المرتبطة، والتحميل المسبق نحويًا يستعلم مباشرة عن []Person، ويحمل أيضًا جميع العلاقات، لكن في الواقع SQL المنفذة متشابهة تقريبًا. مثال
var users []Person
// SELECT * FROM `moms` WHERE `moms`.`id` = 1
// SELECT * FROM `people`
db.Preload("Mom").Find(&users)هذا مثال على استعلام علاقة واحد لواحد، والمخرجات
[{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 للإشارة إلى التحميل المسبق لجميع العلاقات، باستثناء العلاقات المتداخلة.
db.Preload(clause.Associations).Find(&users)مثال على التحميل المسبق المتداخل، وظيفته الاستعلام عن جميع الطلاب المرتبطين بكل مدرسة وأم كل طالب ومنازل كل طالب، وأيضًا مالكو كل منزل، المدرسة -> الطالب -> المنزل -> الطالب.
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، سيتم التراجع تلقائيًا.
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error)مثال، العمليات في الإغلاق يجب أن تستخدم المعامل tx، وليس db الخارجي.
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
})يدوي
يُنصح باستخدام المعاملات اليدوية، حيث نتحكم نحن في متى نتراجع ومتى نُرسل. المعاملات اليدوية ستستخدم الطرق الثلاث التالية
// طريقة Begin لفتح المعاملة
func (db *DB) Begin(opts ...*sql.TxOptions) *DB
// طريقة Rollback للتراجع عن المعاملة
func (db *DB) Rollback() *DB
// طريقة Commit لإرسال المعاملة
func (db *DB) Commit() *DBمثال، بعد فتح المعاملة، يجب استخدام tx للعمل على ORM.
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()يمكن تحديد نقطة تراجع
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 العديد من الوظائف الأخرى، ولمزيد من التفاصيل يمكن الرجوع إلى الوثائق الرسمية.
