عمليات قواعد البيانات
الدرس 27: عمليات قواعد البيانات
تشبيه من الواقع
تخترق أنك تدير مكتبة —
- قاعدة البيانات هي مستودع المكتبة، حيث تُخزّن جميع الكتب (البيانات) بعناية على الرفوف.
- واجهة
database/sqlتشبه دليل أمين المكتبة، تحدد إجراءات معيارية لـ "كيفية وضع الكتب على الرفوف، كيف تجد الكتب، كيف تُعير الكتب" — سواء استخدم المستودع رفوفًا خشبية (SQLite) أو رفوفًا فولاذية (MySQL)، يعمل الدليل بشكل عالمي. - تعريفات التشغيل هي مفاتيح محددة لمستودعات مختلفة — مفتاح SQLite يفتح مستودعًا محليًا صغيرًا، ومفتاح MySQL يفتح مستودعًا بعيدًا كبيرًا.
- العبارات المُعدّة تشبه نماذج الإعارة المطبوعة مسبقًا — تحتاج فقط إلى ملء الاسم وعنوان الكتاب في كل مرة، مما يجعلها سريعة وآمنة في آن واحد.
- المعاملات تشبه عمليات "الإعارة ثم الإرجاع" الذرية — إما أن تكتمل كلها أو ترجع جميعها، لتجنب إحراج "تمت الإعارة ولكن لم تُسجَّل".
بعد هذا الدرس، ستتمكن من العمل مع قواعد البيانات بثقة باستخدام Go.
المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
database/sql |
مكتبة Go القياسية، تحدد واجهة موحدة لعمليات قاعدة البيانات |
| تعريف التشغيل | حزمة طرف ثالث تنفذ واجهة database/sql/driver |
sql.Open() |
ينشئ بركة اتصال قاعدة البيانات (لا يُنشئ اتصالات فورًا) |
sql.DB |
كائن بركة اتصال قاعدة البيانات، آمن للخيوط |
Query / QueryRow |
ينفذان عبارات الاستعلام، يُرجعان مجموعات النتائج |
Exec |
ينفذ عبارات غير استعلامية (INSERT/UPDATE/DELETE) |
Prepare |
عبارات مُعدّة، تحسّن أداء التنفيذ المتكرر |
Tx |
كائن المعاملة، يدعم Commit وRollback |
| بركة الاتصالات | تعيد استخدام الاتصالات، متجنبة تكلفة فتح/إغلاق الاتصالات المتكرر |
بنية الجملة والاستخدام الأساسي
1. استيراد تعريفات التشغيل
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // تعريف تشغيل SQLite (استيراد مجهول، يسجل فقطتعريف التشغيل)
)
_، لأنها فقط تحتاج إلى تنفيذ دالة init() لتسجيل نفسها في database/sql — نحن لا نتصل بدوالتعريف التشغيل مباشرة.
2. فتح قاعدة البيانات
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. إنشاء الجداول
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
// ---- 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. العبارات المُعدّة
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)
6. معالجة المعاملات
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. تكوين بركة الاتصالات
db.SetMaxOpenConns(25) // الحد الأقصى للاتصالات المفتوحة
db.SetMaxIdleConns(10) // الحد الأقصى للاتصالات الخاملة
db.SetConnMaxLifetime(5 * time.Minute) // الحد الأقصى لعمر الاتصال
db.SetConnMaxIdleTime(3 * time.Minute) // الحد الأقصى لعمر الاتصال الخامل
MaxOpenConfs منخفضًا جدًا يسبب اصطفاف الطلبات؛ وتعيينه مرتفعًا جدًا يستنفد موارد قاعدة البيانات.
التطبيق العملي
مثال: إدارة مستخدمي SQLite (الصعوبة ⭐)
مثال كامل لإدارة المستخدمين يُظهر عمليات CRUD الأساسية.
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("تم حذف المستخدم بنجاح")
}
تمت إضافة المستخدم بنجاح، المعرّف: 1
نتيجة الاستعلام: {ID:1 Name:Alice Email:alice@example.com Age:25}
تم التحديث بنجاح
جميع المستخدمين:
المعرّف:1 الاسم:Alice البريد:alice@example.com العمر:26
تم حذف المستخدم بنجاح
مثال: تغليف CRUD + عبارات مُعدّة (الصعوبة ⭐⭐)
يُغلّف عمليات قاعدة البيانات كطرق هياكل، مستخدمًا العبارات المُعدّة لتحسين الأداء.
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)
}
}
تمت الإضافة: 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 سنة
مثال: تحويل المعاملات + تكوين بركة الاتصالات (الصعوبة ⭐⭐⭐)
يُحاكي سيناريو تحويل مصرفي، يُظهر ضمانات ذرية المعاملات وتكوين بركة الاتصالات.
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)
}
✓ تم تكوين بركة الاتصالات:
الحد الأقصى للاتصالات: 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 لتخزين معلومات المستخدم، ويُنفذ التسجيل والتحقق من كلمة المرور.
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: استعلامات مصفحة وإدراج دفعي
يتعامل بكفاءة مع الاستعلامات المصفحة والإدراج الدفعي لكميات كبيرة من البيانات.
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)
}
✓ تم إدراج 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؟
لم يتم استيرادتعريف التشغيل بشكل صحيح. تأكد من استخدام الاستيراد المجهول:
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // هذا السطر مطلوب
)
يعني _ تنفيذ دالة init() فقط للحزمة (تسجيلتعريف التشغيل)، دون الإشارة إلى اسم الحزمة مباشرة. نسيان استيرادتعريف التشغيل هو الخطأ الأكثر شيوعًا للمبتدئين.
س2: لماذا يُبلغ QueryRow().Scan() عن خطأ عندما لا توجد بيانات؟
عندما لا يحتوي الاستعلام على صفوف مطابقة، يُرجع Scan() قيمة sql.ErrNoRows. يجب التعامل معها بشكل منفصل:
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():
func main() {
db, err := sql.Open("sqlite3", "./mydb.db")
if err != nil {
log.Fatal(err)
}
defer db.Close() // تُغلق فقط عند خروج البرنامج
// ... استخدام db لعمليات مختلفة
}
س4: كيف نمنع حقن SQL؟
لا تستخدم أبدًا ربط السلاسل النصية لبناء عبارات SQL. استخدم دائمًا الاستعلامات المُعلمة:
// ❌ خطير! خطر حقن 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 |
| المعاملات | Begin→Exec→Commit/Rollback، تضمن الذرية |
| بركة الاتصالات | SetMaxOpenConns/SetMaxIdleConns وغيرها من طرق الضبط |
Scan |
تُعيّن أعمدة قاعدة البيانات إلى متغيرات Go |
sql.ErrNoRows |
خطأ خاص عندما لا يُرجع الاستعلام نتائج، يحتاج إلى معالجة منفصلة |
بهذه المهارات، لديك القدرة الكاملة على تشغيل قواعد البيانات العلائقية في مشاريع Go.
📝 تمارين
التمرين 1: CRUD أساسي (إحماء)
أنشئ قاعدة بيانات SQLite، أنشئ جدول books (الحقول: id, title, author, price, published_year)، وأكمل العمليات التالية:
- أدرج 5 كتب على الأقل
- استعلم عن كتب سعرها أكبر من 50
- حدّث سعر كتاب
- احذف كتب نُشرت قبل عام 2000
التمرين 2: المعاملات ومعالجة الأخطاء
بناءً على التمرين 1، نفّذ دالة TransferBook: تُحوّل جميع الكتب من مؤلف إلى آخر (استخدم المعاملات لضمان الذرية). المتطلبات:
- إذا لم يكن المؤلف الأصلي موجودًا، تراجع وأرجع خطأ
- بعد التحديث الناجح، اطبع عدد الصفوف المتأثرة
- استخدم
defer+recoverلمنع عمليات الذعر من تسبب تعليقات المعاملات
التمرين 3: تحدي شامل — نظام إدارة كتب مصغر
ابنِ نظام إدارة كتب سطر الأوامر بالمتطلبات التالية:
- دعم عمليات CRUD وقائمة مصفحة
- استخدام نمط DAO لتغليف عمليات قاعدة البيانات
- دعم البحث الغامض بالعنوان والتصفية حسب نطاق السعر
- استخدام العبارات المُعدّة لتحسين الاستعلامات المتكررة
- تكوين معاملات بركة اتصصالات معقولة
- استخدام معالجة أخطاء منظمة بدلاً من
log.Fatal
الدرس التالي: الدرس 28: نشر المشروع — تعلم كيفية تجميع وتعبئة ونشر مشاريع Go على الخوادم.



