عمليات قواعد البيانات

الدرس 27: عمليات قواعد البيانات

تشبيه من الواقع

تخترق أنك تدير مكتبة —

بعد هذا الدرس، ستتمكن من العمل مع قواعد البيانات بثقة باستخدام Go.


المفاهيم الأساسية

المفهوم الوصف
database/sql مكتبة Go القياسية، تحدد واجهة موحدة لعمليات قاعدة البيانات
تعريف التشغيل حزمة طرف ثالث تنفذ واجهة database/sql/driver
sql.Open() ينشئ بركة اتصال قاعدة البيانات (لا يُنشئ اتصالات فورًا)
sql.DB كائن بركة اتصال قاعدة البيانات، آمن للخيوط
Query / QueryRow ينفذان عبارات الاستعلام، يُرجعان مجموعات النتائج
Exec ينفذ عبارات غير استعلامية (INSERT/UPDATE/DELETE)
Prepare عبارات مُعدّة، تحسّن أداء التنفيذ المتكرر
Tx كائن المعاملة، يدعم Commit وRollback
بركة الاتصالات تعيد استخدام الاتصالات، متجنبة تكلفة فتح/إغلاق الاتصالات المتكرر

بنية الجملة والاستخدام الأساسي

1. استيراد تعريفات التشغيل

GO
package main

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3" // تعريف تشغيل SQLite (استيراد مجهول، يسجل فقطتعريف التشغيل)
)
💡 نصيحة: يجب استيرادتعريفات التشغيل بشكل مجهول باستخدام _، لأنها فقط تحتاج إلى تنفيذ دالة init() لتسجيل نفسها في database/sql — نحن لا نتصل بدوالتعريف التشغيل مباشرة.

2. فتح قاعدة البيانات

GO
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

// التحقق من توفر الاتصال
if err = db.Ping(); err != nil {
    log.Fatal("لا يمكن الاتصال بقاعدة البيانات:", err)
}
💡 نصيحة: sql.Open() لا تُنشئ اتصالات فورًا — تقوم فقط بتهيئة تكوين بركة الاتصالات. استخدم Ping() لمحاولة اتصال فعلية.

3. إنشاء الجداول

GO
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    name  TEXT    NOT NULL,
    email TEXT    NOT NULL UNIQUE,
    age   INTEGER DEFAULT 0
);`
_, err = db.Exec(createTableSQL)
if err != nil {
    log.Fatal("فشل في إنشاء الجدول:", err)
}

4. عمليات CRUD

GO
// ---- INSERT ----
result, err := db.Exec("INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
    "Alice", "alice@example.com", 25)
if err != nil {
    log.Fatal(err)
}
id, _ := result.LastInsertId() // الحصول على معرّف الزيادة التلقائية
fmt.Println("تمت الإدراج بنجاح، المعرّف:", id)

// ---- SELECT ----
var name, email string
var age int
err = db.QueryRow("SELECT name, email, age FROM users WHERE id = ?", id).
    Scan(&name, &email, &age)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("نتيجة الاستعلام: %s، %s، %d سنة\n", name, email, age)

// ---- UPDATE ----
_, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
if err != nil {
    log.Fatal(err)
}

// ---- DELETE ----
_, err = db.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
    log.Fatal(err)
}

5. العبارات المُعدّة

GO
stmt, err := db.Prepare("INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()

// يمكن إعادة استخدام نفس stmt عدة مرات لأداء أفضل
stmt.Exec("Bob", "bob@example.com", 30)
stmt.Exec("Charlie", "charlie@example.com", 28)
💡 نصيحة: تحسّن العبارات المُعدّة الأداء (قاعدة البيانات تحلل SQL مرة واحدة فقط) وتمنع حقن SQL.

6. معالجة المعاملات

GO
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}

// تنفيذ عدة عبارات داخل معاملة
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
    tx.Rollback() // التراجع عند الخطأ
    log.Fatal(err)
}

_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
    tx.Rollback()
    log.Fatal(err)
}

if err = tx.Commit(); err != nil { // الالتزام عند النجاح
    log.Fatal(err)
}
fmt.Println("تم الالتزام بالمعاملة بنجاح")

7. تكوين بركة الاتصالات

GO
db.SetMaxOpenConns(25)                 // الحد الأقصى للاتصالات المفتوحة
db.SetMaxIdleConns(10)                 // الحد الأقصى للاتصالات الخاملة
db.SetConnMaxLifetime(5 * time.Minute) // الحد الأقصى لعمر الاتصال
db.SetConnMaxIdleTime(3 * time.Minute) // الحد الأقصى لعمر الاتصال الخامل
💡 نصيحة: يمكن أن يحسّن تكوين بركة الاتصالات المناسب بشكل كبير الأداء في سيناريوهات التزامن العالي. تعيين MaxOpenConfs منخفضًا جدًا يسبب اصطفاف الطلبات؛ وتعيينه مرتفعًا جدًا يستنفد موارد قاعدة البيانات.


التطبيق العملي

مثال: إدارة مستخدمي SQLite (الصعوبة ⭐)

مثال كامل لإدارة المستخدمين يُظهر عمليات CRUD الأساسية.

GO
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3" // تعريف تشغيل SQLite
)

// هيكل المستخدم
type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

func main() {
    // فتح (أو إنشاء) قاعدة بيانات SQLite
    db, err := sql.Open("sqlite3", "./users.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // إنشاء الجدول
    _, err = db.Exec(`
        CREATE TABLE IF NOT EXISTS users (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            age   INTEGER DEFAULT 0
        )
    `)
    if err != nil {
        log.Fatal("فشل في إنشاء الجدول:", err)
    }

    // إدراج مستخدم
    result, err := db.Exec(
        "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
        "Alice", "alice@example.com", 25,
    )
    if err != nil {
        log.Fatal("فشل الإدراج:", err)
    }
    id, _ := result.LastInsertId()
    fmt.Printf("تمت إضافة المستخدم بنجاح، المعرّف: %d\n", id)

    // استعلام مستخدم واحد
    var u User
    err = db.QueryRow("SELECT id, name, email, age FROM users WHERE id = ?", id).
        Scan(&u.ID, &u.Name, &u.Email, &u.Age)
    if err != nil {
        log.Fatal("فشل الاستعلام:", err)
    }
    fmt.Printf("نتيجة الاستعلام: %+v\n", u)

    // تحديث عمر المستخدم
    _, err = db.Exec("UPDATE users SET age = ? WHERE id = ?", 26, id)
    if err != nil {
        log.Fatal("فشل التحديث:", err)
    }
    fmt.Println("تم التحديث بنجاح")

    // استعلام جميع المستخدمين
    rows, err := db.Query("SELECT id, name, email, age FROM users")
    if err != nil {
        log.Fatal("فشل استعلام الكل:", err)
    }
    defer rows.Close()

    fmt.Println("\nجميع المستخدمين:")
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.Age); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("  المعرّف:%d  الاسم:%s  البريد:%s  العمر:%d\n",
            user.ID, user.Name, user.Email, user.Age)
    }

    // حذف مستخدم
    _, err = db.Exec("DELETE FROM users WHERE id = ?", id)
    if err != nil {
        log.Fatal("فشل الحذف:", err)
    }
    fmt.Println("تم حذف المستخدم بنجاح")
}
▶ جرّب الكود
TEXT
تمت إضافة المستخدم بنجاح، المعرّف: 1
نتيجة الاستعلام: {ID:1 Name:Alice Email:alice@example.com Age:25}
تم التحديث بنجاح

جميع المستخدمين:
  المعرّف:1  الاسم:Alice  البريد:alice@example.com  العمر:26
تم حذف المستخدم بنجاح

مثال: تغليف CRUD + عبارات مُعدّة (الصعوبة ⭐⭐)

يُغلّف عمليات قاعدة البيانات كطرق هياكل، مستخدمًا العبارات المُعدّة لتحسين الأداء.

GO
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

// نموذج المستخدم
type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

// UserDAO كائن وصول بيانات المستخدم
type UserDAO struct {
    db         *sql.DB
    insertStmt *sql.Stmt
    getByID    *sql.Stmt
    updateStmt *sql.Stmt
    deleteStmt *sql.Stmt
}

// NewUserDAO ينشئ DAO ويُهيئ العبارات المُعدّة
func NewUserDAO(db *sql.DB) (*UserDAO, error) {
    dao := &UserDAO{db: db}

    // تهيئة الجدول
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS users (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            age   INTEGER DEFAULT 0
        )
    `)
    if err != nil {
        return nil, err
    }

    // تجهيز جميع العبارات الشائعة الاستخدام
    dao.insertStmt, err = db.Prepare(
        "INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
    if err != nil {
        return nil, err
    }

    dao.getByID, err = db.Prepare(
        "SELECT id, name, email, age FROM users WHERE id = ?")
    if err != nil {
        return nil, err
    }

    dao.updateStmt, err = db.Prepare(
        "UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?")
    if err != nil {
        return nil, err
    }

    dao.deleteStmt, err = db.Prepare(
        "DELETE FROM users WHERE id = ?")
    if err != nil {
        return nil, err
    }

    return dao, nil
}

// Close يُغلق جميع العبارات المُعدّة
func (d *UserDAO) Close() {
    d.insertStmt.Close()
    d.getByID.Close()
    d.updateStmt.Close()
    d.deleteStmt.Close()
}

// Create يُدرج مستخدمًا جديدًا
func (d *UserDAO) Create(u *User) error {
    result, err := d.insertStmt.Exec(u.Name, u.Email, u.Age)
    if err != nil {
        return err
    }
    id, err := result.LastInsertId()
    if err != nil {
        return err
    }
    u.ID = int(id)
    return nil
}

// GetByID يُستعلم عن مستخدم حسب المعرّف
func (d *UserDAO) GetByID(id int) (*User, error) {
    u := &User{}
    err := d.getByID.QueryRow(id).Scan(&u.ID, &u.Name, &u.Email, &u.Age)
    if err != nil {
        return nil, err
    }
    return u, nil
}

// Update يُحدّث معلومات المستخدم
func (d *UserDAO) Update(u *User) error {
    _, err := d.updateStmt.Exec(u.Name, u.Email, u.Age, u.ID)
    return err
}

// Delete يحذف مستخدمًا
func (d *UserDAO) Delete(id int) error {
    _, err := d.deleteStmt.Exec(id)
    return err
}

// ListAll يُستعلم عن جميع المستخدمين
func (d *UserDAO) ListAll() ([]User, error) {
    rows, err := d.db.Query("SELECT id, name, email, age FROM users")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age); err != nil {
            return nil, err
        }
        users = append(users, u)
    }
    return users, rows.Err()
}

func main() {
    db, err := sql.Open("sqlite3", ":memory:") // قاعدة بيانات في الذاكرة للعرض
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    dao, err := NewUserDAO(db)
    if err != nil {
        log.Fatal(err)
    }
    defer dao.Close()

    // إدراج دفعي
    users := []User{
        {Name: "Alice", Email: "alice@example.com", Age: 25},
        {Name: "Bob", Email: "bob@example.com", Age: 30},
        {Name: "Charlie", Email: "charlie@example.com", Age: 28},
    }

    for i := range users {
        if err := dao.Create(&users[i]); err != nil {
            log.Printf("فشل الإدراج: %v", err)
            continue
        }
        fmt.Printf("تمت الإضافة: %s (المعرّف=%d)\n", users[i].Name, users[i].ID)
    }

    // الاستعلام والتعديل
    u, err := dao.GetByID(1)
    if err != nil {
        log.Fatal("فشل الاستعلام:", err)
    }
    fmt.Printf("\nاستعلام المعرّف=1: %+v\n", u)

    u.Age = 26
    if err := dao.Update(u); err != nil {
        log.Fatal("فشل التحديث:", err)
    }
    fmt.Printf("بعد التحديث: %+v\n", u)

    // عرض الكل
    all, _ := dao.ListAll()
    fmt.Println("\nجميع المستخدمين:")
    for _, user := range all {
        fmt.Printf("  [%d] %s | %s | %d سنة\n",
            user.ID, user.Name, user.Email, user.Age)
    }

    // الحذف
    dao.Delete(2)
    fmt.Println("\nبعد حذف المعرّف=2:")
    all, _ = dao.ListAll()
    for _, user := range all {
        fmt.Printf("  [%d] %s | %s | %d سنة\n",
            user.ID, user.Name, user.Email, user.Age)
    }
}
▶ جرّب الكود
TEXT
تمت الإضافة: Alice (المعرّف=1)
تمت الإضافة: Bob (المعرّف=2)
تمت الإضافة: Charlie (المعرّف=3)

استعلام المعرّف=1: {ID:1 Name:Alice Email:alice@example.com Age:25}
بعد التحديث: {ID:1 Name:Alice Email:alice@example.com Age:26}

جميع المستخدمين:
  [1] Alice | alice@example.com | 26 سنة
  [2] Bob | bob@example.com | 30 سنة
  [3] Charlie | charlie@example.com | 28 سنة

بعد حذف المعرّف=2:
  [1] Alice | alice@example.com | 26 سنة
  [3] Charlie | charlie@example.com | 28 سنة

مثال: تحويل المعاملات + تكوين بركة الاتصالات (الصعوبة ⭐⭐⭐)

يُحاكي سيناريو تحويل مصرفي، يُظهر ضمانات ذرية المعاملات وتكوين بركة الاتصالات.

GO
package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// Account حساب مصرفي
type Account struct {
    ID      int
    Name    string
    Balance float64
}

func initDB(db *sql.DB) error {
    // إنشاء الجدول
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS accounts (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            name    TEXT    NOT NULL,
            balance REAL    NOT NULL DEFAULT 0
        )
    `)
    return err
}

// Transfer يُنفّذ تحويلًا (عملية معاملة)
// يُحوّل المبلغ من fromID إلى toID
func Transfer(db *sql.DB, fromID, toID int, amount float64) error {
    // بدء المعاملة
    tx, err := db.Begin()
    if err != nil {
        return fmt.Errorf("فشل في بدء المعاملة: %w", err)
    }

    // التأكد من أن المعاملة إما تلتزم أو ترجع
    defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()

    // التحقق من رصيد الحساب المصدر
    var fromBalance float64
    err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", fromID).Scan(&fromBalance)
    if err != nil {
        return fmt.Errorf("فشل في استعلام الحساب المصدر: %w", err)
    }

    if fromBalance < amount {
        return fmt.Errorf("رصيد غير كافٍ: الحالي %.2f، المطلوب %.2f", fromBalance, amount)
    }

    // الخصم من الحساب المصدر
    _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
    if err != nil {
        return fmt.Errorf("فشل الخصم: %w", err)
    }

    // الإضافة إلى الحساب الوجهة
    _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
    if err != nil {
        return fmt.Errorf("فشل الإضافة: %w", err)
    }

    // محاكاة منطق الأعمال: تسجيل سجل العمليات (في نفس المعاملة)
    _, err = tx.Exec(
        "INSERT INTO transfer_log (from_id, to_id, amount, created_at) VALUES (?, ?, ?, ?)",
        fromID, toID, amount, time.Now().Format(time.RFC3339),
    )
    if err != nil {
        // إذا لم يكن جدول السجل موجودًا، تجاهل الخطأ (للعرض فقط)
        fmt.Printf("  [تحذير] فشل تسجيل سجل التحويل: %v\n", err)
    }

    // الالتزام بالمعاملة
    if err = tx.Commit(); err != nil {
        return fmt.Errorf("فشل في الالتزام بالمعاملة: %w", err)
    }

    return nil
}

// PrintBalances يطبع أرصدة جميع الحسابات
func PrintBalances(db *sql.DB) {
    rows, err := db.Query("SELECT id, name, balance FROM accounts ORDER BY id")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    fmt.Println("  أرصدة الحسابات:")
    for rows.Next() {
        var a Account
        if err := rows.Scan(&a.ID, &a.Name, &a.Balance); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("    [%d] %-6s الرصيد: %.2f\n", a.ID, a.Name, a.Balance)
    }
}

func main() {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // تكوين بركة الاتصالات
    db.SetMaxOpenConns(10)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    db.SetConnMaxIdleTime(3 * time.Minute)

    fmt.Println("✓ تم تكوين بركة الاتصالات:")
    fmt.Println("  الحد الأقصى للاتصالات: 10")
    fmt.Println("  الحد الأقصى للخاملة: 5")
    fmt.Println("  عمر الاتصال: 5 دقائق")
    fmt.Println()

    // تهيئة الجداول
    if err := initDB(db); err != nil {
        log.Fatal(err)
    }

    // إنشاء جدول سجل التحويلات (اختياري)
    db.Exec(`CREATE TABLE IF NOT EXISTS transfer_log (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        from_id INTEGER, to_id INTEGER,
        amount REAL, created_at TEXT
    )`)

    // إدراج الحسابات الأولية
    _, err = db.Exec("INSERT INTO accounts (name, balance) VALUES (?, ?), (?, ?)",
        "Alice", 1000.00, "Bob", 500.00)
    if err != nil {
        log.Fatal("فشل في تهيئة الحسابات:", err)
    }

    fmt.Println("=== الحالة الأولية ===")
    PrintBalances(db)
    fmt.Println()

    // تحويل عادي: Alice -> Bob 300
    fmt.Println("=== Alice -> Bob تحويل 300 ===")
    if err := Transfer(db, 1, 2, 300); err != nil {
        fmt.Printf("فشل التحويل: %v\n", err)
    } else {
        fmt.Println("نجح التحويل!")
    }
    PrintBalances(db)
    fmt.Println()

    // تحويل رصيد غير كافٍ: Bob -> Alice 900
    fmt.Println("=== Bob -> Alice تحويل 900 ===")
    if err := Transfer(db, 2, 1, 900); err != nil {
        fmt.Printf("فشل التحويل: %v\n", err)
    } else {
        fmt.Println("نجح التحويل!")
    }
    PrintBalances(db)
    fmt.Println()

    // تحويل عادي آخر
    fmt.Println("=== Bob -> Alice تحويل 100 ===")
    if err := Transfer(db, 2, 1, 100); err != nil {
        fmt.Printf("فشل التحويل: %v\n", err)
    } else {
        fmt.Println("نجح التحويل!")
    }
    PrintBalances(db)
}
▶ جرّب الكود
TEXT
✓ تم تكوين بركة الاتصالات:
  الحد الأقصى للاتصالات: 10
  الحد الأقصى للخاملة: 5
  عمر الاتصال: 5 دقائق

=== الحالة الأولية ===
  أرصدة الحسابات:
    [1] Alice الرصيد: 1000.00
    [2] Bob   الرصيد: 500.00

=== Alice -> Bob تحويل 300 ===
نجح التحويل!
  أرصدة الحسابات:
    [1] Alice الرصيد: 700.00
    [2] Bob   الرصيد: 800.00

=== Bob -> Alice تحويل 900 ===
فشل التحويل: رصيد غير كافٍ: الحالي 800.00، المطلوب 900.00
  أرصدة الحسابات:
    [1] Alice الرصيد: 700.00
    [2] Bob   الرصيد: 800.00

=== Bob -> Alice تحويل 100 ===
نجح التحويل!
  أرصدة الحسابات:
    [1] Alice الرصيد: 800.00
    [2] Bob   الرصيد: 700.00

سيناريوهات واقعية

السيناريو 1: تسجيل المستخدم وتسجيل الدخول (MySQL)

يستخدم MySQL لتخزين معلومات المستخدم، ويُنفذ التسجيل والتحقق من كلمة المرور.

GO
package main

import (
    "crypto/sha256"
    "database/sql"
    "encoding/hex"
    "fmt"
    "log"
    "time"

    _ "github.com/go-sql-driver/mysql" // تعريف تشغيل MySQL
)

// hashPassword تشفير بسيط لكلمة المرور (في الإنتاج يجب استخدام bcrypt)
func hashPassword(password string) string {
    h := sha256.Sum256([]byte(password))
    return hex.EncodeToString(h[:])
}

// UserRepository مستودع المستخدمين
type UserRepository struct {
    db *sql.DB
}

func NewUserRepository(dsn string) (*UserRepository, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, err
    }

    // تكوين بركة الاتصالات
    db.SetMaxOpenConns(20)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(10 * time.Minute)

    if err := db.Ping(); err != nil {
        return nil, fmt.Errorf("فشل الاتصال بقاعدة البيانات: %w", err)
    }

    return &UserRepository{db: db}, nil
}

func (r *UserRepository) Close() error {
    return r.db.Close()
}

// Register يُسجل مستخدمًا جديدًا
func (r *UserRepository) Register(username, password, email string) (int64, error) {
    hashedPwd := hashPassword(password)

    result, err := r.db.Exec(
        "INSERT INTO users (username, password_hash, email, created_at) VALUES (?, ?, ?, ?)",
        username, hashedPwd, email, time.Now(),
    )
    if err != nil {
        return 0, fmt.Errorf("فشل التسجيل: %w", err)
    }

    return result.LastInsertId()
}

// Login يتحقق من تسجيل دخول المستخدم
func (r *UserRepository) Login(username, password string) (int, error) {
    hashedPwd := hashPassword(password)

    var id int
    var storedHash string

    err := r.db.QueryRow(
        "SELECT id, password_hash FROM users WHERE username = ?",
        username,
    ).Scan(&id, &storedHash)

    if err == sql.ErrNoRows {
        return 0, fmt.Errorf("المستخدم غير موجود")
    }
    if err != nil {
        return 0, fmt.Errorf("فشل الاستعلام: %w", err)
    }

    if storedHash != hashedPwd {
        return 0, fmt.Errorf("كلمة المرور غير صحيحة")
    }

    // تحديث آخر وقت دخول
    _, _ = r.db.Exec(
        "UPDATE users SET last_login = ? WHERE id = ?",
        time.Now(), id,
    )

    return id, nil
}

func main() {
    // تنسيق DSN: username:password@tcp(host:port)/dbname?parseTime=true
    dsn := "root:password@tcp(127.0.0.1:3306)/myapp?parseTime=true"

    repo, err := NewUserRepository(dsn)
    if err != nil {
        log.Fatal("فشل التهيئة:", err)
    }
    defer repo.Close()

    // التسجيل
    id, err := repo.Register("alice", "mypassword123", "alice@example.com")
    if err != nil {
        log.Fatal("فشل التسجيل:", err)
    }
    fmt.Printf("نجح التسجيل، معرّف المستخدم: %d\n", id)

    // تسجيل الدخول
    userID, err := repo.Login("alice", "mypassword123")
    if err != nil {
        fmt.Printf("فشل تسجيل الدخول: %v\n", err)
    } else {
        fmt.Printf("نجح تسجيل الدخول، معرّف المستخدم: %d\n", userID)
    }

    // كلمة مرور خاطئة
    _, err = repo.Login("alice", "wrongpassword")
    if err != nil {
        fmt.Printf("فشل تسجيل الدخول: %v\n", err)
    }
}

السيناريو 2: استعلامات مصفحة وإدراج دفعي

يتعامل بكفاءة مع الاستعلامات المصفحة والإدراج الدفعي لكميات كبيرة من البيانات.

GO
package main

import (
    "database/sql"
    "fmt"
    "log"
    "strings"

    _ "github.com/mattn/go-sqlite3"
)

// Product منتج
type Product struct {
    ID    int
    Name  string
    Price float64
    Stock int
}

// PageResult نتيجة التصفح
type PageResult struct {
    Items      []Product
    Total      int
    Page       int
    PageSize   int
    TotalPages int
}

// ProductStore تخزين المنتجات
type ProductStore struct {
    db *sql.DB
}

func NewProductStore(db *sql.DB) (*ProductStore, error) {
    _, err := db.Exec(`
        CREATE TABLE IF NOT EXISTS products (
            id    INTEGER PRIMARY KEY AUTOINCREMENT,
            name  TEXT NOT NULL,
            price REAL NOT NULL,
            stock INTEGER DEFAULT 0
        )
    `)
    if err != nil {
        return nil, err
    }
    return &ProductStore{db: db}, nil
}

// BatchInsert إدراج دفعي (يستخدم المعاملة لأداء أفضل)
func (s *ProductStore) BatchInsert(products []Product) error {
    tx, err := s.db.Begin()
    if err != nil {
        return err
    }

    stmt, err := tx.Prepare("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)")
    if err != nil {
        tx.Rollback()
        return err
    }
    defer stmt.Close()

    for _, p := range products {
        _, err := stmt.Exec(p.Name, p.Price, p.Stock)
        if err != nil {
            tx.Rollback()
            return fmt.Errorf("فشل إدراج %s: %w", p.Name, err)
        }
    }

    return tx.Commit()
}

// Paginate استعلام مصفح
func (s *ProductStore) Paginate(page, pageSize int) (*PageResult, error) {
    if page < 1 {
        page = 1
    }
    if pageSize < 1 {
        pageSize = 10
    }

    // استعلام العدد الكلي
    var total int
    err := s.db.QueryRow("SELECT COUNT(*) FROM products").Scan(&total)
    if err != nil {
        return nil, err
    }

    // حساب عدد الصفحات
    totalPages := total / pageSize
    if total%pageSize > 0 {
        totalPages++
    }

    // استعلام بيانات الصفحة الحالية
    offset := (page - 1) * pageSize
    rows, err := s.db.Query(
        "SELECT id, name, price, stock FROM products ORDER BY id LIMIT ? OFFSET ?",
        pageSize, offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var items []Product
    for rows.Next() {
        var p Product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
            return nil, err
        }
        items = append(items, p)
    }

    return &PageResult{
        Items:      items,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    }, nil
}

// Search بحث غامض بالاسم
func (s *ProductStore) Search(keyword string, page, pageSize int) (*PageResult, error) {
    likePattern := "%" + keyword + "%"

    var total int
    err := s.db.QueryRow(
        "SELECT COUNT(*) FROM products WHERE name LIKE ?", likePattern,
    ).Scan(&total)
    if err != nil {
        return nil, err
    }

    totalPages := total / pageSize
    if total%pageSize > 0 {
        totalPages++
    }

    offset := (page - 1) * pageSize
    rows, err := s.db.Query(
        "SELECT id, name, price, stock FROM products WHERE name LIKE ? ORDER BY id LIMIT ? OFFSET ?",
        likePattern, pageSize, offset,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var items []Product
    for rows.Next() {
        var p Product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price, &p.Stock); err != nil {
            return nil, err
        }
        items = append(items, p)
    }

    return &PageResult{
        Items:      items,
        Total:      total,
        Page:       page,
        PageSize:   pageSize,
        TotalPages: totalPages,
    }, nil
}

func printPage(result *PageResult) {
    fmt.Printf("  الصفحة %d/%d (المجموع %d عنصر)\n",
        result.Page, result.TotalPages, result.Total)
    for _, p := range result.Items {
        fmt.Printf("    [%d] %-10s السعر: %6.2f  المخزون: %d\n",
            p.ID, p.Name, p.Price, p.Stock)
    }
}

func main() {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    store, err := NewProductStore(db)
    if err != nil {
        log.Fatal(err)
    }

    // إدراج 50 منتجًا دفعيًا
    var products []Product
    names := []string{"لوحة مفاتيح", "فأرة", "شاشة", "سماعات", "مكبر صوت",
        "كاميرا ويب", "قرص USB", "قرص صلب", "ذاكرة RAM", "لوحة أم"}
    for i := 0; i < 50; i++ {
        products = append(products, Product{
            Name:  fmt.Sprintf("%s-%02d", names[i%len(names)], i+1),
            Price: float64(50 + i*10),
            Stock: 100 - i,
        })
    }

    if err := store.BatchInsert(products); err != nil {
        log.Fatal("فشل الإدراج الدفعي:", err)
    }
    fmt.Printf("✓ تم إدراج %d منتجًا دفعيًا بنجاح\n\n", len(products))

    // استعلام مصفح
    fmt.Println("=== استعلام مصفح (10 لكل صفحة) ===")
    for page := 1; page <= 3; page++ {
        result, err := store.Paginate(page, 10)
        if err != nil {
            log.Fatal(err)
        }
        printPage(result)
        fmt.Println()
    }

    // البحث
    fmt.Println("=== البحث عن 'لوحة مفاتيح' ===")
    result, err := store.Search("لوحة مفاتيح", 1, 10)
    if err != nil {
        log.Fatal(err)
    }
    printPage(result)
}
TEXT
✓ تم إدراج 50 منتجًا دفعيًا بنجاح

=== استعلام مصفح (10 لكل صفحة) ===
  الصفحة 1/5 (المجموع 50 عنصر)
    [1] لوحة مفاتيح-01  السعر:  50.00  المخزون: 100
    [2] فأرة-02         السعر:  60.00  المخزون: 99
    ...

=== البحث عن 'لوحة مفاتيح' ===
  الصفحة 1/1 (المجموع 5 عنصر)
    [1] لوحة مفاتيح-01  السعر:  50.00  المخزون: 100
    [11] لوحة مفاتيح-11 السعر: 150.00  المخزون: 90
    [21] لوحة مفاتيح-21 السعر: 250.00  المخزون: 80
    [31] لوحة مفاتيح-31 السعر: 350.00  المخزون: 70
    [41] لوحة مفاتيح-41 السعر: 450.00  المخزون: 60

❓ أسئلة شائعة

س1: لماذا يُبلغ sql.Open() عن unknown driver؟

لم يتم استيرادتعريف التشغيل بشكل صحيح. تأكد من استخدام الاستيراد المجهول:

GO
import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3" // هذا السطر مطلوب
)

يعني _ تنفيذ دالة init() فقط للحزمة (تسجيلتعريف التشغيل)، دون الإشارة إلى اسم الحزمة مباشرة. نسيان استيرادتعريف التشغيل هو الخطأ الأكثر شيوعًا للمبتدئين.

س2: لماذا يُبلغ QueryRow().Scan() عن خطأ عندما لا توجد بيانات؟

عندما لا يحتوي الاستعلام على صفوف مطابقة، يُرجع Scan() قيمة sql.ErrNoRows. يجب التعامل معها بشكل منفصل:

GO
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = ?", 999).Scan(&name)
if err == sql.ErrNoRows {
    fmt.Println("المستخدم غير موجود")
} else if err != nil {
    log.Fatal("خطأ الاستعلام:", err)
}

س3: متى يجب استدعاء db.Close()؟

sql.DB هي بركة اتصالات ويجب أن تبقى مفتوحة طوال دورة حياة البرنامج، لا تُغلق بعد كل عملية. عادةً استخدم defer db.Close() في بداية دالة main():

GO
func main() {
    db, err := sql.Open("sqlite3", "./mydb.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close() // تُغلق فقط عند خروج البرنامج

    // ... استخدام db لعمليات مختلفة
}

س4: كيف نمنع حقن SQL؟

لا تستخدم أبدًا ربط السلاسل النصية لبناء عبارات SQL. استخدم دائمًا الاستعلامات المُعلمة:

GO
// ❌ خطير! خطر حقن SQL
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput)

// ✓ آمن: استخدم أماكن معلمة
db.Query("SELECT * FROM users WHERE name = ?", userInput)

مكان الحامل ? يتم تشفيره تلقائيًا من قبلتعريف التشغيل، وتعامل معه قاعدة البيانات كبيانات خام بدلاً من كود SQL.


📖 ملخص

في هذا الدرس تعلمنا:

الموضوع النقاط الرئيسية
database/sql واجهة موحدة في مكتبة Go القياسية، تدعم أي تعريف تشغيل قاعدة بيانات
استيرادتعريف التشغيل يجب استخدام _ للاستيراد المجهول، يُسجّل فقطتعريف التشغيل
sql.Open ينشئ بركة اتصالات، لا يتصل فورًا؛ استخدم Ping() للتحقق
CRUD Exec لعمليات الكتابة، Query/QueryRow لقراءة البيانات
العبارات المُعدّة Prepare تحسّن الأداء، تمنع حقن SQL
المعاملات BeginExecCommit/Rollback، تضمن الذرية
بركة الاتصالات SetMaxOpenConns/SetMaxIdleConns وغيرها من طرق الضبط
Scan تُعيّن أعمدة قاعدة البيانات إلى متغيرات Go
sql.ErrNoRows خطأ خاص عندما لا يُرجع الاستعلام نتائج، يحتاج إلى معالجة منفصلة

بهذه المهارات، لديك القدرة الكاملة على تشغيل قواعد البيانات العلائقية في مشاريع Go.


📝 تمارين

التمرين 1: CRUD أساسي (إحماء)

أنشئ قاعدة بيانات SQLite، أنشئ جدول books (الحقول: id, title, author, price, published_year)، وأكمل العمليات التالية:

  1. أدرج 5 كتب على الأقل
  2. استعلم عن كتب سعرها أكبر من 50
  3. حدّث سعر كتاب
  4. احذف كتب نُشرت قبل عام 2000

التمرين 2: المعاملات ومعالجة الأخطاء

بناءً على التمرين 1، نفّذ دالة TransferBook: تُحوّل جميع الكتب من مؤلف إلى آخر (استخدم المعاملات لضمان الذرية). المتطلبات:

التمرين 3: تحدي شامل — نظام إدارة كتب مصغر

ابنِ نظام إدارة كتب سطر الأوامر بالمتطلبات التالية:

  1. دعم عمليات CRUD وقائمة مصفحة
  2. استخدام نمط DAO لتغليف عمليات قاعدة البيانات
  3. دعم البحث الغامض بالعنوان والتصفية حسب نطاق السعر
  4. استخدام العبارات المُعدّة لتحسين الاستعلامات المتكررة
  5. تكوين معاملات بركة اتصصالات معقولة
  6. استخدام معالجة أخطاء منظمة بدلاً من log.Fatal

الدرس التالي: الدرس 28: نشر المشروع — تعلم كيفية تجميع وتعبئة ونشر مشاريع Go على الخوادم.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%