مقارنة ORMs لـ Go لـ PostgreSQL: GORM مقابل Ent مقابل Bun مقابل sqlc

نظرة عملية ومليئة بالكود على ORMs في GO

Page content

أبرز ORMs لـ GO هي GORM، Ent، Bun و sqlc. هنا مقارنة بسيطة بينها مع أمثلة على عمليات CRUD في GO النقي.

golang + postgresql

TL;DR

  • GORM: ميزات كثيرة ومريحة؛ الأسهل لـ “just ship”، لكنه يحتوي على مزيد من التحميل أثناء التشغيل.
  • Ent: نموذج الشيفرة مع واجهات API مُولدة ومُحصنة نوعًا؛ مثالي لقواعد البيانات الكبيرة والتعديلات.
  • Bun: خفيف، مُنشئ استعلامات SQL أولاً/ORM؛ سريع مع ميزات Postgres الرائعة، صريح بالتصميم.
  • sqlc (ليس ORM بالمعنى الحرفي ولكن لا يزال): اكتب SQL، احصل على Go مُحصنة نوعًا؛ الأداء الأفضل والتحكم الأكبر، بدون سحر أثناء التشغيل.

معايير الاختيار والمقارنة السريعة

معاييري هي:

  • الأداء: التأخير/التدفق، التحميل القابل لتجنبه، العمليات الجماعية.
  • تجربة المطور (DX): منحنى التعلم، الأمان النوعي، قابلية التصحيح، مقاومة إنشاء الشيفرة.
  • البيئة: الوثائق، الأمثلة، النشاط، التكاملات (التحديثات، تتبع الأداء).
  • مجموعة الميزات: العلاقات، التحميل المبكر، التحديثات، التصحيحات، مخارج SQL نقي.
الأداة المنهجية الأمان النوعي العلاقات التحديثات سهولة استخدام SQL الخام حالة الاستخدام النموذجية
GORM ORM من نوع Active Record متوسط (أثناء التشغيل) نعم (العلامات، Preload/Joins) التحديث التلقائي (اختياري) db.Raw(...) تسليم سريع، ميزات غنية، تطبيقات CRUD التقليدية
Ent النموذج → إنشاء الشيفرة → واجهة API سلسة مرتفع (أثناء التجميع) أولوية (الحواف) SQL مُولدة (خطوة منفصلة) entsql، SQL مخصص قواعد بيانات كبيرة، فرق تعديلات كثيرة، تقييد نوعي صارم
Bun مُنشئ استعلامات SQL أولاً/ORM متوسط–مرتفع صريح (Relation) حزمة منفصلة للتحديثات طبيعي (المُنشئ + الخام) خدمات مهتمة بالأداء، ميزات Postgres
sqlc SQL → إنشاء دوال (ليس ORM بالمعنى الحرفي) مرتفع (أثناء التجميع) عبر توصيلات SQL أداة خارجية (مثل golang-migrate) هو SQL التحكم والسرعة الأقصى؛ فرق مهتمة بـ DBA

CRUD عبر أمثلة

الإعداد (PostgreSQL)

استخدم pgx أو محرك PG الخاص بالأداة. مثال DSN:

export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'

المستورات (شائعة لجميع ORMs)

في بداية كل ملف مع كود GO مثال أضف:

import (
  "context"
  "os"
)

سنتعامل مع جدول بسيط users:

CREATE TABLE IF NOT EXISTS users (
  id    BIGSERIAL PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

GORM

البدء

import (
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
)

type User struct {
  ID    int64  `gorm:"primaryKey"`
  Name  string
  Email string `gorm:"uniqueIndex"`
}

func newGorm() (*gorm.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}

// التحديث التلقائي (اختياري؛ كن حذرًا في الإنتاج)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

func gormCRUD(ctx context.Context, db *gorm.DB) error {
  // إنشاء
  u := User{Name: "Alice", Email: "alice@example.com"}
  if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }

  // القراءة
  var got User
  if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }

  // التحديث
  if err := db.WithContext(ctx).Model(&got).
    Update("email", "alice+1@example.com").Error; err != nil { return err }

  // الحذف
  if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }

  return nil
}

ملاحظات

  • العلاقات عبر العلامات + Preload/Joins.
  • مساعدة التحويل: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

تعريف النموذج (في ent/schema/user.go):

package schema

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

type User struct {
  ent.Schema
}

func (User) Fields() []ent.Field {
  return []ent.Field{
    field.Int64("id").Unique().Immutable(),
    field.String("name"),
    field.String("email").Unique(),
  }
}

إنشاء الشيفرة

go run entgo.io/ent/cmd/ent generate ./ent/schema

البدء

import (
  "entgo.io/ent/dialect"
  "entgo.io/ent/dialect/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/ent"
)

func newEnt() (*ent.Client, error) {
  dsn := os.Getenv("DATABASE_URL")
  drv, err := sql.Open(dialect.Postgres, dsn)
  if err != nil { return nil, err }
  return ent.NewClient(ent.Driver(drv)), nil
}

CRUD

func entCRUD(ctx context.Context, client *ent.Client) error {
  // إنشاء
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

  // القراءة
  got, err := client.User.Get(ctx, u.ID)
  if err != nil { return err }

  // التحديث
  if _, err := client.User.UpdateOneID(got.ID).
    SetEmail("alice+1@example.com").
    Save(ctx); err != nil { return err }

  // الحذف
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

ملاحظات

  • الأمان النوعي من البداية إلى النهاية؛ الحواف للعلاقات.
  • تحديثات مُولدة أو استخدم أداة التحديث المفضلة لديك.

Bun

البدء

import (
  "database/sql"

  "github.com/uptrace/bun"
  "github.com/uptrace/bun/dialect/pgdialect"
  _ "github.com/jackc/pgx/v5/stdlib"
)

type User struct {
  bun.BaseModel `bun:"table:users"`
  ID    int64  `bun:",pk,autoincrement"`
  Name  string `bun:",notnull"`
  Email string `bun:",unique,notnull"`
}

func newBun() (*bun.DB, error) {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return nil, err }
  return bun.NewDB(sqldb, pgdialect.New()), nil
}

CRUD

func bunCRUD(ctx context.Context, db *bun.DB) error {
  // إنشاء
  u := &User{Name: "Alice", Email: "alice@example.com"}
  if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }

  // القراءة
  var got User
  if err := db.NewSelect().Model(&got).
    Where("id = ?", u.ID).
    Scan(ctx); err != nil { return err }

  // التحديث
  if _, err := db.NewUpdate().Model(&got).
    Set("email = ?", "alice+1@example.com").
    WherePK().
    Exec(ctx); err != nil { return err }

  // الحذف
  if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }

  return nil
}

ملاحظات

  • التوصيلات/التحميل المبكر صريح مع .Relation("...").
  • حزمة منفصلة bun/migrate للتحديثات.

sqlc

sqlc تقنيًا ليس ORM. تكتب SQL؛ تُولد دوال Go مُحصنة نوعًا.

sqlc.yaml

version: "2"
sql:
  - engine: postgresql
    queries: db/queries
    schema: db/migrations
    gen:
      go:
        package: db
        out: internal/db
        sql_package: "database/sql" # أو "github.com/jackc/pgx/v5"

الاستعلامات (db/queries/users.sql)

-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;

-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;

-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;

-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;

إنشاء

sqlc generate

الاستخدام

import (
  "database/sql"
  _ "github.com/jackc/pgx/v5/stdlib"
  "your/module/internal/db"
)

func sqlcCRUD(ctx context.Context) error {
  dsn := os.Getenv("DATABASE_URL")
  sqldb, err := sql.Open("pgx", dsn)
  if err != nil { return err }
  q := db.New(sqldb)

  // إنشاء
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

  // القراءة
  got, err := q.GetUser(ctx, u.ID)
  if err != nil { return err }

  // التحديث
  up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
    ID: got.ID, Email: "alice+1@example.com",
  })
  if err != nil { return err }

  // الحذف
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

ملاحظات

  • استخدم أدوات التحديث الخاصة بك (مثل golang-migrate).
  • للأسئلة الديناميكية: اكتب عدة نسخ من SQL أو ادمج مع مُنشئ صغير.

ملاحظات الأداء

  • GORM: مريح لكنه يضيف تحميلًا عبر التكرار/الإسقاط. مناسب للـ CRUD العادي؛ كن حذرًا من استعلامات N+1 (تفضيل Joins أو Preload انتقائية).
  • Ent: الشيفرة المُولدة تتجنب التكرار؛ جيد لـ النماذج المعقدة. غالبًا أسرع من ORMs الثقيلة ذات السحر أثناء التشغيل.
  • Bun: رقيق فوق database/sql؛ سريع، صريح، مثالي للعمليات الجماعية والنتائج الكبيرة.
  • sqlc: أداء SQL الخام تقريبًا مع الأمان أثناء التجميع.

نصائح عامة

  • استخدم pgx كمحرك (v5) وcontext في كل مكان.
  • تفضيل التوسيع (COPY، إدراج متعدد الصفوف) للتدفق العالي.
  • تحليل SQL: EXPLAIN ANALYZE، الفهارس، الفهارس المغطاة، تجنب الزيارات غير الضرورية.
  • إعادة استخدام الاتصالات؛ ضبط حجم المجموعة بناءً على الحمل.

تجربة المطور والبيئة

  • GORM: أكبر مجتمع، العديد من الأمثلة/الإضافات؛ منحنى تعلم أصعب للأنماط المتقدمة.
  • Ent: وثائق رائعة؛ خطوة إنشاء الشيفرة هي التحول الرئيسي في العقل؛ مثالي للفرق التي تعيد التصميم.
  • Bun: استعلامات قابلة للقراءة وتنبؤية؛ مجتمع أصغر لكن نشط؛ ميزات Postgres رائعة.
  • sqlc: تبعات تشغيلية قليلة؛ تتكامل جيدًا مع أدوات التحديث والـ CI؛ مثالي للفرق التي تفضل SQL النقي.

مميزات البارزة

  • العلاقات والتحميل المبكر: جميعها تتعامل مع العلاقات؛ GORM (العلامات + Preload/Joins)، Ent (الحواف + .With...())، Bun (Relation(...))، sqlc (تكتب التوصيلات).
  • التحديثات: GORM (تحديث تلقائي؛ كن حذرًا في الإنتاج)، Ent (SQL مُولدة/الاختلافات)، Bun (bun/migrate)، sqlc (أدوات خارجية).
  • التصحيحات/التوسع: GORM (التصحيحات/الإضافات)، Ent (التصحيحات/الوسيط + النماذج/الشيفرة)، Bun (تصحيحات استعلام وسائط + SQL نقي سهل)، sqlc (دمج في طبقة التطبيق).
  • النصوص/المصفوفات (Postgres): Bun و GORM لهما مساعدين جيدين؛ Ent/sqlc تتعامل عبر أنواع مخصصة أو SQL.

متى تختار ماذا

  • اختر GORM إذا كنت ترغب في الراحة القصوى، الميزات الغنية، والتطوير السريع لتطبيقات CRUD التقليدية.
  • اختر Ent إذا كنت تفضل الأمان أثناء التجميع، النماذج صريحة، والصيانة طويلة الأمد في الفرق الكبيرة.
  • اختر Bun إذا كنت ترغب في الأداء والأسئلة SQL صريحة مع ميزات ORM حيث تساعد.
  • اختر sqlc إذا كنت أنت وفريقك تفضلون SQL النقي مع روابط Go مُحصنة نوعًا بدون تحميل أثناء التشغيل.

docker-compose.yml أدنى لـ PostgreSQL محلي

version: "3.8"
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d app"]
      interval: 5s
      timeout: 3s
      retries: 5

حزم ORM وتوسعات في GO

روابط مفيدة أخرى