پایگاه داده در Go: اتصال به PostgreSQL و MySQL

فهرست مطالب

پایگاه داده در Go: اتصال به PostgreSQL و MySQL

در دنیای توسعه نرم‌افزار مدرن، مدیریت کارآمد داده‌ها ستون فقرات هر اپلیکیشن قوی و مقیاس‌پذیر است. زبان برنامه‌نویسی Go، با تمرکز بر کارایی، همروندی (Concurrency) و سادگی، به سرعت به انتخابی محبوب برای ساخت سرویس‌های بک‌اند و سیستم‌های با کارایی بالا تبدیل شده است. در قلب بسیاری از این سیستم‌ها، پایگاه‌های داده رابطه‌ای مانند PostgreSQL و MySQL قرار دارند که به دلیل قابلیت اطمینان، پایداری و مجموعه‌ای غنی از ویژگی‌ها شناخته شده‌اند.

این مقاله جامع، راهنمایی عمیق برای اتصال و تعامل با پایگاه‌های داده PostgreSQL و MySQL با استفاده از Go ارائه می‌دهد. ما از پکیج هسته‌ای database/sql شروع می‌کنیم که پایه و اساس هرگونه تعامل با پایگاه داده در Go است، سپس به بررسی درایورهای خاص برای هر دو پایگاه داده می‌پردازیم. علاوه بر این، ابزارها و تکنیک‌های پیشرفته‌تر مانند مدیریت تراکنش‌ها، استفاده از ORMها و Query Builderها، و بهترین شیوه‌ها برای توسعه پایگاه داده را پوشش خواهیم داد. هدف این مقاله، توانمندسازی توسعه‌دهندگان Go با دانش و ابزارهای لازم برای ساخت برنامه‌های پایگاه داده محور قوی و کارآمد است.

مقدمه‌ای بر Go و مدیریت پایگاه داده

انتخاب Go برای مدیریت پایگاه داده، تصمیمی استراتژیک برای پروژه‌هایی است که به کارایی بالا، مقیاس‌پذیری و نگهداری آسان نیاز دارند. Go با ویژگی‌هایی مانند همروندی از طریق Goroutineها و کانال‌ها، کامپایل بهینه‌شده به باینری‌های استاتیک، و یک اکوسیستم استاندارد و قدرتمند، محیطی ایده‌آل برای سرویس‌های ارتباط با پایگاه داده فراهم می‌کند.

چرا Go برای مدیریت پایگاه داده؟

  • کارایی بالا: Go به دلیل زمان کامپایل سریع و اجرای کارآمد، برای سرویس‌های با بار کاری بالا (High-load services) و میکروسرویس‌ها بسیار مناسب است. این ویژگی‌ها در مدیریت اتصالات پایگاه داده و پردازش کوئری‌ها به بهترین شکل خود را نشان می‌دهند.
  • همروندی آسان: Goroutineها و کانال‌ها، امکان مدیریت هزاران اتصال همزمان به پایگاه داده را بدون پیچیدگی‌های مربوط به ریسه‌ها فراهم می‌کنند، که برای برنامه‌های وب مقیاس‌پذیر حیاتی است.
  • سادگی و خوانایی: سینتکس ساده و رویکرد مستقیم Go به برنامه‌نویسی، کد پایگاه داده را خوانا و قابل نگهداری می‌کند.
  • پکیج استاندارد قوی: پکیج database/sql یک انتزاع قدرتمند و در عین حال انعطاف‌پذیر برای کار با انواع پایگاه داده رابطه‌ای فراهم می‌کند.
  • اکوسیستم در حال رشد: جامعه Go به سرعت در حال توسعه ابزارهای ORM، ابزارهای مهاجرت (Migration tools) و درایورهای پایگاه داده است که کار با Go را برای توسعه‌دهندگان آسان‌تر می‌کند.

نقش پکیج database/sql در Go

پکیج database/sql در کتابخانه استاندارد Go، یک رابط (interface) عمومی برای تعامل با پایگاه‌های داده رابطه‌ای ارائه می‌دهد. این پکیج به تنهایی شامل درایورهای پایگاه داده خاص نیست؛ بلکه یک لایه انتزاعی فراهم می‌کند که به توسعه‌دهندگان اجازه می‌دهد کد عمومی بنویسند که می‌تواند با هر پایگاه داده‌ای که دارای درایور Go سازگار است، کار کند. این جدایی بین رابط عمومی و پیاده‌سازی‌های خاص درایور، یکی از نقاط قوت کلیدی Go در کار با پایگاه داده است.

وقتی شما از database/sql استفاده می‌کنید، در واقع با اینترفیس‌هایی مانند sql.DB، sql.Conn، sql.Stmt، sql.Rows و sql.Tx سروکار دارید. هر درایور پایگاه داده‌ای که برای Go نوشته می‌شود (مانند github.com/lib/pq برای PostgreSQL یا github.com/go-sql-driver/mysql برای MySQL)، باید اینترفیس‌های لازم را پیاده‌سازی کند تا database/sql بتواند با آن ارتباط برقرار کند.

تفاوت database/sql با ORMها

در حالی که database/sql یک رابط سطح پایین برای کار با پایگاه داده ارائه می‌دهد، ORMها (Object-Relational Mappers) یک لایه انتزاعی بالاتر هستند که به شما اجازه می‌دهند با داده‌ها به عنوان اشیاء (Objects) یا ساختارها (Structs) در زبان برنامه‌نویسی خود کار کنید، نه به عنوان ردیف‌ها و ستون‌های جدول. این بدان معناست که شما کمتر با SQL خام سروکار دارید و بیشتر بر روی مدل‌های داده‌ای خود تمرکز می‌کنید.

  • database/sql (Native/Low-level):
    • کنترل کامل: به شما اجازه می‌دهد کوئری‌های SQL را به صورت دستی بنویسید و کنترل دقیقی بر عملکرد و بهینه‌سازی داشته باشید.
    • یادگیری بیشتر: نیازمند دانش عمیق‌تر از SQL و نحوه کار پایگاه داده است.
    • Code Verbosity: ممکن است نیاز به نوشتن کد boilerplate بیشتری برای نگاشت نتایج کوئری به ساختارهای Go داشته باشد.
    • انعطاف‌پذیری: بسیار انعطاف‌پذیر برای سناریوهای پیچیده و خاص.
  • ORMها (High-level Abstraction):
    • توسعه سریع‌تر: با مدل‌سازی داده‌ها به صورت structها و استفاده از توابع ORM، می‌توان به سرعت عملیات CRUD (Create, Read, Update, Delete) را انجام داد.
    • کاهش SQL خام: کوئری‌ها به صورت خودکار توسط ORM تولید می‌شوند.
    • تجزیه (Parsing) خودکار: نتایج به صورت خودکار به ساختارهای Go نگاشت می‌شوند.
    • پیچیدگی پنهان: گاهی اوقات ORMها می‌توانند کوئری‌های ناکارآمد تولید کنند و دیباگ کردن مشکلات عملکردی دشوارتر باشد.
    • وابستگی به ORM: شما به API یک ORM خاص وابسته می‌شوید.

انتخاب بین database/sql و یک ORM بستگی به نیازهای پروژه، اندازه تیم، و اولویت‌های عملکردی و توسعه دارد. در این مقاله، هر دو رویکرد را مورد بررسی قرار خواهیم داد.

پکیج database/sql: ستون فقرات اتصال به پایگاه داده

پکیج database/sql اصلی‌ترین ابزار شما برای کار با پایگاه داده در Go است. این پکیج یک API یکپارچه برای دسترسی به پایگاه داده فراهم می‌کند، فارغ از اینکه از چه پایگاه داده رابطه‌ای (مانند PostgreSQL، MySQL، SQLite و غیره) استفاده می‌کنید. این انعطاف‌پذیری با استفاده از مفهوم “درایور” (Driver) به دست می‌آید.

درایورهای پایگاه داده در Go

برای اتصال به یک پایگاه داده خاص، شما نیاز به یک درایور شخص ثالث دارید که اینترفیس‌های database/sql را پیاده‌سازی کرده باشد. دو درایور محبوب برای PostgreSQL و MySQL عبارتند از:

برای استفاده از این درایورها، کافیست آن‌ها را با دستور go get دانلود کنید و سپس در کد خود آن‌ها را import کنید. استفاده از یک import با نام مستعار underscore (_) به Go می‌گوید که پکیج را صرفاً برای اثرات جانبی آن (یعنی ثبت درایور در database/sql) بارگذاری کند، بدون اینکه هیچ تابعی از آن پکیج را به صورت مستقیم استفاده کند.

اتصال و قطع اتصال (Open, Close)

اولین گام برای تعامل با پایگاه داده، ایجاد یک اتصال است. این کار با تابع sql.Open() انجام می‌شود.


package main

import (
	"database/sql"
	"fmt"
	_ "github.com/lib/pq"    // PostgreSQL driver
	_ "github.com/go-sql-driver/mysql" // MySQL driver
	"log"
)

func main() {
	// DSN برای PostgreSQL
	pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"
	dbPG, err := sql.Open("postgres", pgDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL: %v", err)
	}
	defer dbPG.Close() // حتماً اتصال را در پایان کار ببندید

	err = dbPG.Ping()
	if err != nil {
		log.Fatalf("خطا در Ping به PostgreSQL: %v", err)
	}
	fmt.Println("با موفقیت به PostgreSQL متصل شدید!")

	// DSN برای MySQL
	mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"
	dbMySQL, err := sql.Open("mysql", mysqlDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به MySQL: %v", err)
	}
	defer dbMySQL.Close() // حتماً اتصال را در پایان کار ببندید

	err = dbMySQL.Ping()
	if err != nil {
		log.Fatalf("خطا در Ping به MySQL: %v", err)
	}
	fmt.Println("با موفقیت به MySQL متصل شدید!")
}

تابع sql.Open() دو پارامتر می‌گیرد: نام درایور (مانند “postgres” یا “mysql”) و یک رشته DSN (Data Source Name). DSN حاوی اطلاعات لازم برای اتصال به پایگاه داده است (نام کاربری، رمز عبور، هاست، پورت، نام پایگاه داده و غیره). دقت کنید که sql.Open() بلافاصله یک اتصال به پایگاه داده ایجاد نمی‌کند؛ بلکه تنها شیء *sql.DB را برمی‌گرداند که یک انتزاع از اتصال به پایگاه داده است. اولین اتصال واقعی زمانی برقرار می‌شود که یک کوئری یا عملیات دیگری روی db انجام دهید (مانند db.Ping()).

استفاده از defer db.Close() بسیار مهم است تا اطمینان حاصل شود که اتصال پایگاه داده در پایان اجرای تابع فعلی بسته می‌شود و منابع آزاد می‌گردند.

مدیریت Connection Pool

شیء *sql.DB در Go یک Connection Pool داخلی را مدیریت می‌کند. این بدان معناست که Go اتصالات به پایگاه داده را باز نگه می‌دارد و آن‌ها را برای استفاده مجدد در اختیار قرار می‌دهد، به جای اینکه برای هر کوئری یک اتصال جدید ایجاد و سپس آن را ببندد. این کار به طور قابل توجهی سربار عملکردی را کاهش می‌دهد.

شما می‌توانید رفتار Connection Pool را با توابع زیر پیکربندی کنید:

  • db.SetMaxOpenConns(n int): حداکثر تعداد اتصالات بازی که می‌توانند به پایگاه داده باز باشند (شامل اتصالات استفاده شده و اتصالات بیکار).
  • db.SetMaxIdleConns(n int): حداکثر تعداد اتصالات بیکار (Idle) در Connection Pool. اگر اتصالات بیکار بیشتری از این مقدار وجود داشته باشد، Go آن‌ها را می‌بندد.
  • db.SetConnMaxLifetime(d time.Duration): حداکثر مدت زمانی که یک اتصال می‌تواند باز باشد. پس از این مدت، اتصال بسته و یک اتصال جدید جایگزین آن می‌شود. این برای مقابله با مشکلات مربوط به اتصالات قدیمی (مانند Timeoutهای پایگاه داده یا تغییر رمز عبور) مفید است.

	// پیکربندی Connection Pool
	dbPG.SetMaxOpenConns(20) // حداکثر 20 اتصال باز
	dbPG.SetMaxIdleConns(10) // حداکثر 10 اتصال بیکار
	dbPG.SetConnMaxLifetime(5 * time.Minute) // عمر هر اتصال حداکثر 5 دقیقه

پیکربندی بهینه این پارامترها به بار کاری اپلیکیشن و قابلیت‌های پایگاه داده شما بستگی دارد. مقادیر پیش‌فرض ممکن است برای همه سناریوها مناسب نباشند.

اجرای کوئری‌ها

database/sql توابعی را برای اجرای انواع مختلف کوئری‌ها ارائه می‌دهد:

  • DB.Exec(query string, args ...any) (Result, error): برای اجرای کوئری‌های INSERT, UPDATE, DELETE که ردیفی را برنمی‌گردانند.
    • Result.RowsAffected(): تعداد ردیف‌های تحت تأثیر.
    • Result.LastInsertId(): آی‌دی آخرین ردیف درج شده (همه درایورها پشتیبانی نمی‌کنند).
  • DB.Query(query string, args ...any) (*Rows, error): برای اجرای کوئری‌های SELECT که انتظار چندین ردیف نتیجه را دارند.
    • نتیجه یک *sql.Rows است که باید آن را پیمایش کنید.
    • حتماً Rows.Close() را فراخوانی کنید تا منابع آزاد شوند.
  • DB.QueryRow(query string, args ...any) *Row: برای اجرای کوئری‌های SELECT که انتظار تنها یک ردیف نتیجه را دارند.
    • این تابع مستقیماً یک خطا برنمی‌گرداند؛ خطا در زمان فراخوانی Scan() برای خواندن نتیجه رخ می‌دهد.

استفاده از آرگومان‌ها (args ...any) در این توابع به شدت توصیه می‌شود. این کار از SQL Injection جلوگیری می‌کند و به پایگاه داده اجازه می‌دهد کوئری‌های آماده (Prepared Statements) را بهینه کند.


package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"
	_ "github.com/lib/pq"
	_ "github.com/go-sql-driver/mysql"
)

// User represents a user in the database
type User struct {
	ID    int
	Name  string
	Email string
}

func main() {
	// PostgreSQL Configuration
	pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"
	dbPG, err := sql.Open("postgres", pgDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL: %v", err)
	}
	defer dbPG.Close()
	dbPG.SetMaxOpenConns(20)
	dbPG.SetMaxIdleConns(10)
	dbPG.SetConnMaxLifetime(5 * time.Minute)
	if err = dbPG.Ping(); err != nil {
		log.Fatalf("خطا در Ping به PostgreSQL: %v", err)
	}
	fmt.Println("با موفقیت به PostgreSQL متصل شدید و Connection Pool پیکربندی شد.")

	// MySQL Configuration
	mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"
	dbMySQL, err := sql.Open("mysql", mysqlDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به MySQL: %v", err)
	}
	defer dbMySQL.Close()
	dbMySQL.SetMaxOpenConns(20)
	dbMySQL.SetMaxIdleConns(10)
	dbMySQL.SetConnMaxLifetime(5 * time.Minute)
	if err = dbMySQL.Ping(); err != nil {
		log.Fatalf("خطا در Ping به MySQL: %v", err)
	}
	fmt.Println("با موفقیت به MySQL متصل شدید و Connection Pool پیکربندی شد.")

	// Example: Create table (PostgreSQL)
	createTablePG(dbPG)
	// Example: Create table (MySQL)
	createTableMySQL(dbMySQL)

	// Example: Insert data (PostgreSQL)
	insertUserPG(dbPG, "علی", "ali@example.com")
	insertUserPG(dbPG, "سارا", "sara@example.com")

	// Example: Insert data (MySQL)
	insertUserMySQL(dbMySQL, "رضا", "reza@example.com")
	insertUserMySQL(dbMySQL, "مریم", "maryam@example.com")

	// Example: Query multiple rows (PostgreSQL)
	fmt.Println("\nکاربران PostgreSQL:")
	queryUsersPG(dbPG)

	// Example: Query multiple rows (MySQL)
	fmt.Println("\nکاربران MySQL:")
	queryUsersMySQL(dbMySQL)

	// Example: Query single row (PostgreSQL)
	fmt.Println("\nکاربر با آی‌دی 1 در PostgreSQL:")
	querySingleUserPG(dbPG, 1)

	// Example: Query single row (MySQL)
	fmt.Println("\nکاربر با آی‌دی 1 در MySQL:")
	querySingleUserMySQL(dbMySQL, 1)

	// Example: Update data (PostgreSQL)
	updateUserPG(dbPG, 1, "علی جدید", "new_ali@example.com")
	fmt.Println("\nکاربر با آی‌دی 1 در PostgreSQL بعد از به‌روزرسانی:")
	querySingleUserPG(dbPG, 1)

	// Example: Update data (MySQL)
	updateUserMySQL(dbMySQL, 1, "رضا جدید", "new_reza@example.com")
	fmt.Println("\nکاربر با آی‌دی 1 در MySQL بعد از به‌روزرسانی:")
	querySingleUserMySQL(dbMySQL, 1)

	// Example: Delete data (PostgreSQL)
	deleteUserPG(dbPG, 2)
	fmt.Println("\nکاربران PostgreSQL بعد از حذف:")
	queryUsersPG(dbPG)

	// Example: Delete data (MySQL)
	deleteUserMySQL(dbMySQL, 2)
	fmt.Println("\nکاربران MySQL بعد از حذف:")
	queryUsersMySQL(dbMySQL)
}

func createTablePG(db *sql.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS users (
		id SERIAL PRIMARY KEY,
		name VARCHAR(100) NOT NULL,
		email VARCHAR(100) UNIQUE NOT NULL
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول users در PostgreSQL: %v", err)
	}
	fmt.Println("جدول users در PostgreSQL با موفقیت ایجاد یا موجود بود.")
}

func createTableMySQL(db *sql.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS users (
		id INT AUTO_INCREMENT PRIMARY KEY,
		name VARCHAR(100) NOT NULL,
		email VARCHAR(100) UNIQUE NOT NULL
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول users در MySQL: %v", err)
	}
	fmt.Println("جدول users در MySQL با موفقیت ایجاد یا موجود بود.")
}

func insertUserPG(db *sql.DB, name, email string) {
	query := `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id;`
	var id int
	err := db.QueryRow(query, name, email).Scan(&id)
	if err != nil {
		log.Printf("خطا در درج کاربر %s در PostgreSQL: %v", name, err)
		return
	}
	fmt.Printf("کاربر %s با آی‌دی %d در PostgreSQL درج شد.\n", name, id)
}

func insertUserMySQL(db *sql.DB, name, email string) {
	query := `INSERT INTO users (name, email) VALUES (?, ?);`
	result, err := db.Exec(query, name, email)
	if err != nil {
		log.Printf("خطا در درج کاربر %s در MySQL: %v", name, err)
		return
	}
	id, _ := result.LastInsertId()
	fmt.Printf("کاربر %s با آی‌دی %d در MySQL درج شد.\n", name, id)
}

func queryUsersPG(db *sql.DB) {
	rows, err := db.Query(`SELECT id, name, email FROM users;`)
	if err != nil {
		log.Printf("خطا در کوئری کاربران PostgreSQL: %v", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var user User
		if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
			log.Printf("خطا در اسکن ردیف PostgreSQL: %v", err)
			continue
		}
		fmt.Printf("ID: %d, Name: %s, Email: %s\n", user.ID, user.Name, user.Email)
	}
	if err = rows.Err(); err != nil {
		log.Printf("خطا پس از پیمایش ردیف‌ها در PostgreSQL: %v", err)
	}
}

func queryUsersMySQL(db *sql.DB) {
	rows, err := db.Query(`SELECT id, name, email FROM users;`)
	if err != nil {
		log.Printf("خطا در کوئری کاربران MySQL: %v", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var user User
		if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {
			log.Printf("خطا در اسکن ردیف MySQL: %v", err)
			continue
		}
		fmt.Printf("ID: %d, Name: %s, Email: %s\n", user.ID, user.Name, user.Email)
	}
	if err = rows.Err(); err != nil {
		log.Printf("خطا پس از پیمایش ردیف‌ها در MySQL: %v", err)
	}
}

func querySingleUserPG(db *sql.DB, id int) {
	var user User
	err := db.QueryRow(`SELECT id, name, email FROM users WHERE id = $1;`, id).Scan(&user.ID, &user.Name, &user.Email)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Printf("کاربر با آی‌دی %d در PostgreSQL یافت نشد.\n", id)
			return
		}
		log.Printf("خطا در کوئری کاربر با آی‌دی %d در PostgreSQL: %v", id, err)
		return
	}
	fmt.Printf("ID: %d, Name: %s, Email: %s\n", user.ID, user.Name, user.Email)
}

func querySingleUserMySQL(db *sql.DB, id int) {
	var user User
	err := db.QueryRow(`SELECT id, name, email FROM users WHERE id = ?;`, id).Scan(&user.ID, &user.Name, &user.Email)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Printf("کاربر با آی‌دی %d در MySQL یافت نشد.\n", id)
			return
		}
		log.Printf("خطا در کوئری کاربر با آی‌دی %d در MySQL: %v", id, err)
		return
	}
	fmt.Printf("ID: %d, Name: %s, Email: %s\n", user.ID, user.Name, user.Email)
}

func updateUserPG(db *sql.DB, id int, newName, newEmail string) {
	result, err := db.Exec(`UPDATE users SET name = $1, email = $2 WHERE id = $3;`, newName, newEmail, id)
	if err != nil {
		log.Printf("خطا در به‌روزرسانی کاربر با آی‌دی %d در PostgreSQL: %v", id, err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("کاربر با آی‌دی %d در PostgreSQL به‌روزرسانی شد. ردیف‌های تحت تأثیر: %d\n", id, rowsAffected)
}

func updateUserMySQL(db *sql.DB, id int, newName, newEmail string) {
	result, err := db.Exec(`UPDATE users SET name = ?, email = ? WHERE id = ?;`, newName, newEmail, id)
	if err != nil {
		log.Printf("خطا در به‌روزرسانی کاربر با آی‌دی %d در MySQL: %v", id, err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("کاربر با آی‌دی %d در MySQL به‌روزرسانی شد. ردیف‌های تحت تأثیر: %d\n", id, rowsAffected)
}

func deleteUserPG(db *sql.DB, id int) {
	result, err := db.Exec(`DELETE FROM users WHERE id = $1;`, id)
	if err != nil {
		log.Printf("خطا در حذف کاربر با آی‌دی %d در PostgreSQL: %v", id, err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("کاربر با آی‌دی %d در PostgreSQL حذف شد. ردیف‌های تحت تأثیر: %d\n", id, rowsAffected)
}

func deleteUserMySQL(db *sql.DB, id int) {
	result, err := db.Exec(`DELETE FROM users WHERE id = ?;`, id)
	if err != nil {
		log.Printf("خطا در حذف کاربر با آی‌دی %d در MySQL: %v", id, err)
		return
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("کاربر با آی‌دی %d در MySQL حذف شد. ردیف‌های تحت تأثیر: %d\n", id, rowsAffected)
}

در این مثال، مشاهده می‌کنید که چگونه توابع Exec، Query و QueryRow برای انجام عملیات CRUD (ایجاد، خواندن، به‌روزرسانی، حذف) مورد استفاده قرار می‌گیرند. نکات مهم عبارتند از:

  • PostgreSQL از $1, $2, ... برای پارامترها استفاده می‌کند، در حالی که MySQL از ?, ? و غیره استفاده می‌کند.
  • در تابع insertUserPG، از RETURNING id برای دریافت ID آخرین ردیف درج شده در PostgreSQL استفاده شده است. در MySQL، این کار با result.LastInsertId() انجام می‌شود.
  • برای کوئری‌هایی که چندین ردیف برمی‌گردانند، باید از یک حلقه for rows.Next() برای پیمایش نتایج استفاده کنید و سپس با rows.Scan() مقادیر را به متغیرهای Go نگاشت کنید. حتماً پس از اتمام کار با rows، تابع rows.Close() را فراخوانی کنید.
  • برای کوئری‌هایی که یک ردیف برمی‌گردانند، مستقیماً از QueryRow().Scan() استفاده کنید. اگر ردیفی یافت نشد، Scan() خطای sql.ErrNoRows را برمی‌گرداند.

اتصال و کار با PostgreSQL در Go

PostgreSQL یک سیستم مدیریت پایگاه داده رابطه‌ای شی‌گرا (Object-Relational Database Management System – ORDBMS) قدرتمند، منبع باز و پیشرفته است که به دلیل قابلیت اطمینان، پایداری و مجموعه‌ای غنی از ویژگی‌ها شناخته شده است. در Go، درایور github.com/lib/pq استاندارد و پرکاربردترین درایور برای PostgreSQL است.

نصب درایور PostgreSQL

برای شروع، درایور pq را با دستور زیر نصب کنید:


go get github.com/lib/pq

سپس آن را در کد خود import کنید:


import (
	"database/sql"
	_ "github.com/lib/pq" // underscore import
	// ... سایر importها
)

DSN برای PostgreSQL

رشته DSN (Data Source Name) برای PostgreSQL معمولاً شامل موارد زیر است:

  • host: آدرس سرور پایگاه داده (مانند localhost یا یک IP).
  • port: پورت پایگاه داده (پیش‌فرض: 5432).
  • user: نام کاربری پایگاه داده.
  • password: رمز عبور کاربر.
  • dbname: نام پایگاه داده‌ای که می‌خواهید به آن متصل شوید.
  • sslmode: نحوه استفاده از SSL (مانند disable، require، verify-full). برای توسعه محلی معمولاً disable استفاده می‌شود، اما برای محیط‌های تولید باید آن را به درستی پیکربندی کنید.

مثال:


pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"

مثال کامل: اتصال، ایجاد جدول، درج، خواندن، به روزرسانی، حذف (PostgreSQL)

کد نمونه‌ای که در بخش قبل ارائه شد، شامل تمامی عملیات CRUD برای PostgreSQL است. در آن مثال، تابع createTablePG یک جدول users با یک ستون SERIAL PRIMARY KEY برای ID ایجاد می‌کند که به صورت خودکار افزایش می‌یابد. در تابع insertUserPG نیز از RETURNING id برای بازگرداندن ID جدید استفاده شده است که یک ویژگی رایج در PostgreSQL است.

مدیریت خطاهای خاص PostgreSQL (PQError)

درایور github.com/lib/pq یک نوع خطای خاص به نام pq.Error (با نام مستعار pgconn.PgError در نسخه‌های جدیدتر) را فراهم می‌کند که می‌توانید آن را برای بررسی خطاهای پایگاه داده (مانند نقض محدودیت‌های UNIQUE یا FOREIGN KEY) استفاده کنید. این خطا شامل فیلدهایی مانند Code (کد خطای SQLSTATE) و Detail است.


package main

import (
	"database/sql"
	"fmt"
	"log"
	"github.com/lib/pq" // Make sure to import pq directly

	_ "github.com/lib/pq"
)

func main() {
	pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"
	db, err := sql.Open("postgres", pgDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL: %v", err)
	}
	defer db.Close()

	if err = db.Ping(); err != nil {
		log.Fatalf("خطا در Ping به PostgreSQL: %v", err)
	}

	createTableQuery := `
	CREATE TABLE IF NOT EXISTS products (
		id SERIAL PRIMARY KEY,
		name VARCHAR(100) UNIQUE NOT NULL,
		price NUMERIC(10, 2) NOT NULL
	);`
	_, err = db.Exec(createTableQuery)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول products: %v", err)
	}
	fmt.Println("جدول products با موفقیت ایجاد یا موجود بود.")

	insertProduct := func(name string, price float64) {
		query := `INSERT INTO products (name, price) VALUES ($1, $2);`
		_, err := db.Exec(query, name, price)
		if err != nil {
			if pgErr, ok := err.(*pq.Error); ok {
				// SQLSTATE 23505 is unique_violation
				if pgErr.Code == "23505" {
					fmt.Printf("خطا: محصول '%s' از قبل وجود دارد. (کد SQLSTATE: %s)\n", name, pgErr.Code)
					return
				}
			}
			log.Printf("خطا در درج محصول '%s': %v\n", name, err)
			return
		}
		fmt.Printf("محصول '%s' با موفقیت درج شد.\n", name)
	}

	insertProduct("لپ تاپ", 1200.00)
	insertProduct("موس", 25.00)
	insertProduct("لپ تاپ", 1500.00) // این باید خطا دهد به دلیل UNIQUE constraint
}

با استفاده از type assertion if pgErr, ok := err.(*pq.Error); ok، می‌توانید خطای برگشتی را به نوع pq.Error تبدیل کرده و به جزئیات آن دسترسی پیدا کنید، از جمله کد خطای SQLSTATE که در این مثال برای تشخیص نقض Unique constraint استفاده شده است.

اتصال و کار با MySQL در Go

MySQL یکی دیگر از محبوب‌ترین سیستم‌های مدیریت پایگاه داده رابطه‌ای است که به طور گسترده در برنامه‌های وب و سیستم‌های تجاری مورد استفاده قرار می‌گیرد. در Go، درایور github.com/go-sql-driver/mysql به عنوان پیاده‌سازی رسمی و توصیه شده برای MySQL عمل می‌کند.

نصب درایور MySQL

برای شروع، درایور MySQL را با دستور زیر نصب کنید:


go get github.com/go-sql-driver/mysql

سپس آن را در کد خود import کنید:


import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql" // underscore import
	// ... سایر importها
)

DSN برای MySQL

ساختار DSN برای MySQL کمی متفاوت است و معمولاً شامل:

  • user:password@tcp(host:port)/dbname: بخش‌های اصلی برای احراز هویت و آدرس پایگاه داده.
  • charset=utf8mb4: برای پشتیبانی از کاراکترهای یونیکد (از جمله ایموجی‌ها).
  • parseTime=True: برای تبدیل خودکار ستون‌های DATETIME/TIMESTAMP MySQL به نوع time.Time در Go. بدون این پارامتر، این ستون‌ها به []byte تبدیل می‌شوند.
  • loc=Local: برای استفاده از منطقه زمانی محلی هنگام تجزیه زمان‌ها.

مثال:


mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"

مثال کامل: اتصال، ایجاد جدول، درج، خواندن، به روزرسانی، حذف (MySQL)

همانطور که در بخش “اجرای کوئری‌ها” دیدید، کد نمونه شامل تمامی عملیات CRUD برای MySQL نیز هست. تفاوت‌های کلیدی در اینجا عبارتند از:

  • نوع AUTO_INCREMENT PRIMARY KEY برای ID در MySQL.
  • استفاده از ? به جای $1 برای پارامترها در کوئری‌ها.
  • بازیابی LastInsertId() پس از عملیات درج.

برخلاف PostgreSQL که از RETURNING id پشتیبانی می‌کند، در MySQL برای گرفتن آخرین ID درج شده باید از متد LastInsertId() روی شیء sql.Result استفاده کنید. این متد فقط برای ستون‌های AUTO_INCREMENT معتبر است.

نکات و تفاوت‌ها با PostgreSQL

  • پلی‌س‌هولدرها (Placeholders): PostgreSQL از $1, $2, ... و MySQL از ?, ?, ... استفاده می‌کند.
  • درج ID: PostgreSQL با RETURNING id کارآمدتر است، در حالی که MySQL به LastInsertId() متکی است.
  • انواع داده‌ای: اگرچه هر دو پایگاه داده انواع داده‌ای مشابهی دارند، اما تفاوت‌های ظریفی وجود دارد (مثلاً SERIAL در PostgreSQL در مقابل AUTO_INCREMENT در MySQL).
  • مدیریت زمان: در DSN MySQL، استفاده از parseTime=True&loc=Local برای مدیریت صحیح time.Time در Go حیاتی است. PostgreSQL این کار را به صورت پیش‌فرض بهتر انجام می‌دهد.
  • خطاهای خاص: برای MySQL، می‌توانید از mysql.MySQLError برای بررسی جزئیات خطاها استفاده کنید (با import کردن پکیج github.com/go-sql-driver/mysql و type casting).

مدیریت تراکنش‌ها (Transactions) در Go

تراکنش‌ها (Transactions) مکانیزم‌هایی حیاتی در پایگاه داده‌های رابطه‌ای هستند که به شما امکان می‌دهند چندین عملیات SQL را به صورت یک واحد منطقی و اتمی گروه بندی کنید. مفهوم ACID (Atomicity, Consistency, Isolation, Durability) اصول پایه‌ای تراکنش‌ها را تشکیل می‌دهد و اطمینان می‌دهد که داده‌ها حتی در صورت بروز خطا یا قطع برق، سازگار و قابل اعتماد باقی می‌مانند.

چرا تراکنش‌ها مهم هستند؟ (ACID properties)

  • اتمی بودن (Atomicity): یک تراکنش یا به طور کامل انجام می‌شود (Commit) یا به طور کامل لغو می‌شود (Rollback). هیچ حالت نیمه‌کاره‌ای وجود ندارد. اگر حتی یک عملیات در تراکنش شکست بخورد، تمام عملیات‌های قبلی لغو می‌شوند.
  • سازگاری (Consistency): یک تراکنش داده‌ها را از یک حالت معتبر به حالت معتبر دیگری منتقل می‌کند و قوانین یکپارچگی پایگاه داده (مانند محدودیت‌های Unique، Foreign Key) را حفظ می‌کند.
  • جداسازی (Isolation): عملیات یک تراکنش در حالی که در حال انجام است، از عملیات سایر تراکنش‌ها جدا هستند. این بدان معناست که یک تراکنش در حال اجرا، اثرات موقتی بر سایر تراکنش‌ها نمی‌گذارد تا زمانی که به طور کامل Commit شود.
  • پایداری (Durability): پس از اینکه یک تراکنش با موفقیت Commit شد، تغییرات آن دائمی هستند و حتی در صورت خرابی سیستم (مانند قطع برق)، حفظ می‌شوند.

یک سناریوی رایج برای استفاده از تراکنش‌ها، انتقال پول بین دو حساب بانکی است. این عملیات شامل کاهش موجودی از یک حساب و افزایش آن در حساب دیگر است. اگر یکی از این عملیات‌ها شکست بخورد، باید هر دو عملیات لغو شوند تا پول نه از بین برود و نه دو برابر شود.

DB.BeginTx() و Tx.Commit(), Tx.Rollback()

در Go، تراکنش‌ها با استفاده از متد BeginTx() روی شیء *sql.DB آغاز می‌شوند. این متد یک شیء *sql.Tx را برمی‌گرداند که نماینده تراکنش شماست. تمام عملیات‌های پایگاه داده‌ای که در داخل یک تراکنش انجام می‌شوند، باید روی این شیء *sql.Tx فراخوانی شوند، نه روی شیء *sql.DB.

  • db.BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error): شروع یک تراکنش. ctx برای لغو تراکنش و opts برای تنظیم سطح ایزوله‌سازی و حالت فقط خواندنی (read-only) است.
  • tx.Commit() error: تمام تغییرات انجام شده در تراکنش را دائمی می‌کند.
  • tx.Rollback() error: تمام تغییرات انجام شده در تراکنش را لغو می‌کند.

یک الگوی رایج برای مدیریت تراکنش در Go به شکل زیر است:


tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return err
}
// مطمئن شوید که تراکنش در صورت بروز خطا Rollback می‌شود.
// اگر Commit با موفقیت انجام شود، Rollback کاری انجام نمی‌دهد.
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
        panic(r) // Re-throw the panic
    } else if err != nil {
        tx.Rollback() // Rollback if there was an error
    }
}()

// عملیات‌های پایگاه داده را روی 'tx' انجام دهید
// ...
_, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2;", amount, fromAccountID)
if err != nil {
    return err // defer will handle rollback
}
// ...
_, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2;", amount, toAccountID)
if err != nil {
    return err // defer will handle rollback
}

// در صورت موفقیت آمیز بودن تمام عملیات‌ها، Commit کنید
err = tx.Commit()
if err != nil {
    return err
}
// اگر به اینجا برسید، تراکنش با موفقیت انجام شده است

استفاده از defer tx.Rollback() یک الگوی قوی است، زیرا تضمین می‌کند که تراکنش در صورت بروز هر گونه خطا در طول عملیات، لغو می‌شود. اگر tx.Commit() با موفقیت فراخوانی شود، فراخوانی بعدی tx.Rollback() بی‌اثر خواهد بود.

مثال عملی با تراکنش

فرض کنید می‌خواهیم یک عملیات انتقال وجه از یک حساب به حساب دیگر را شبیه‌سازی کنیم:


package main

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

	_ "github.com/lib/pq"
	_ "github.com/go-sql-driver/mysql"
)

// Account represents a bank account
type Account struct {
	ID      int
	Balance float64
}

func main() {
	// PostgreSQL Configuration
	pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"
	dbPG, err := sql.Open("postgres", pgDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL: %v", err)
	}
	defer dbPG.Close()
	dbPG.SetMaxOpenConns(10)
	dbPG.SetConnMaxLifetime(5 * time.Minute)
	if err = dbPG.Ping(); err != nil {
		log.Fatalf("خطا در Ping به PostgreSQL: %v", err)
	}
	fmt.Println("با موفقیت به PostgreSQL متصل شدید.")
	setupAccountsTablePG(dbPG)
	seedAccountsPG(dbPG)
	transferMoneyPG(dbPG, 1, 2, 50.0) // انتقال 50 دلار از حساب 1 به حساب 2
	transferMoneyPG(dbPG, 1, 2, 5000.0) // انتقال ناموفق به دلیل موجودی ناکافی
	getAccountBalancesPG(dbPG)

	// MySQL Configuration
	mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"
	dbMySQL, err := sql.Open("mysql", mysqlDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به MySQL: %v", err)
	}
	defer dbMySQL.Close()
	dbMySQL.SetMaxOpenConns(10)
	dbMySQL.SetConnMaxLifetime(5 * time.Minute)
	if err = dbMySQL.Ping(); err != nil {
		log.Fatalf("خطا در Ping به MySQL: %v", err)
	}
	fmt.Println("\nبا موفقیت به MySQL متصل شدید.")
	setupAccountsTableMySQL(dbMySQL)
	seedAccountsMySQL(dbMySQL)
	transferMoneyMySQL(dbMySQL, 1, 2, 30.0) // انتقال 30 دلار از حساب 1 به حساب 2
	transferMoneyMySQL(dbMySQL, 1, 2, 3000.0) // انتقال ناموفق به دلیل موجودی ناکافی
	getAccountBalancesMySQL(dbMySQL)
}

func setupAccountsTablePG(db *sql.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS accounts (
		id SERIAL PRIMARY KEY,
		balance NUMERIC(10, 2) NOT NULL
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول accounts در PostgreSQL: %v", err)
	}
	fmt.Println("جدول accounts در PostgreSQL با موفقیت ایجاد یا موجود بود.")
}

func setupAccountsTableMySQL(db *sql.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS accounts (
		id INT AUTO_INCREMENT PRIMARY KEY,
		balance DECIMAL(10, 2) NOT NULL
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول accounts در MySQL: %v", err)
	}
	fmt.Println("جدول accounts در MySQL با موفقیت ایجاد یا موجود بود.")
}

func seedAccountsPG(db *sql.DB) {
	// Clear table first to ensure fresh data for example
	_, _ = db.Exec("TRUNCATE TABLE accounts RESTART IDENTITY;")
	_, err := db.Exec(`INSERT INTO accounts (balance) VALUES (1000.00), (500.00);`)
	if err != nil {
		log.Fatalf("خطا در درج داده اولیه در PostgreSQL: %v", err)
	}
	fmt.Println("داده‌های اولیه در PostgreSQL درج شد.")
}

func seedAccountsMySQL(db *sql.DB) {
	_, _ = db.Exec("TRUNCATE TABLE accounts;")
	_, err := db.Exec(`INSERT INTO accounts (balance) VALUES (1000.00), (500.00);`)
	if err != nil {
		log.Fatalf("خطا در درج داده اولیه در MySQL: %v", err)
	}
	fmt.Println("داده‌های اولیه در MySQL درج شد.")
}

func transferMoneyPG(db *sql.DB, fromAccountID, toAccountID int, amount float64) {
	ctx := context.Background()
	tx, err := db.BeginTx(ctx, nil) // شروع تراکنش
	if err != nil {
		log.Printf("خطا در شروع تراکنش PostgreSQL: %v", err)
		return
	}

	// defer Rollback in case of an error or panic.
	// If Commit is called successfully, Rollback will be a no-op.
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			panic(r) // re-throw panic after Rollback
		} else if err != nil {
			tx.Rollback() // Rollback if an error occurred during operations
		}
	}()

	// 1. موجودی حساب فرستنده را بررسی کنید
	var fromBalance float64
	err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE;", fromAccountID).Scan(&fromBalance)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Printf("خطا در انتقال PostgreSQL: حساب مبدا %d یافت نشد.\n", fromAccountID)
			return
		}
		log.Printf("خطا در خواندن موجودی حساب مبدا در PostgreSQL: %v", err)
		return
	}

	if fromBalance < amount {
		fmt.Printf("خطا در انتقال PostgreSQL: موجودی حساب %d (%f) کافی نیست برای %f.\n", fromAccountID, fromBalance, amount)
		return // defer will rollback
	}

	// 2. موجودی حساب فرستنده را کاهش دهید
	_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - $1 WHERE id = $2;", amount, fromAccountID)
	if err != nil {
		log.Printf("خطا در کاهش موجودی حساب %d در PostgreSQL: %v", fromAccountID, err)
		return
	}

	// 3. موجودی حساب گیرنده را افزایش دهید
	_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + $1 WHERE id = $2;", amount, toAccountID)
	if err != nil {
		log.Printf("خطا در افزایش موجودی حساب %d در PostgreSQL: %v", toAccountID, err)
		return
	}

	// 4. تراکنش را Commit کنید
	err = tx.Commit()
	if err != nil {
		log.Printf("خطا در Commit تراکنش PostgreSQL: %v", err)
		return
	}

	fmt.Printf("انتقال %f از حساب %d به حساب %d در PostgreSQL با موفقیت انجام شد.\n", amount, fromAccountID, toAccountID)
}


func transferMoneyMySQL(db *sql.DB, fromAccountID, toAccountID int, amount float64) {
	ctx := context.Background()
	tx, err := db.BeginTx(ctx, nil) // شروع تراکنش
	if err != nil {
		log.Printf("خطا در شروع تراکنش MySQL: %v", err)
		return
	}

	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			panic(r)
		} else if err != nil {
			tx.Rollback()
		}
	}()

	var fromBalance float64
	err = tx.QueryRowContext(ctx, "SELECT balance FROM accounts WHERE id = ? FOR UPDATE;", fromAccountID).Scan(&fromBalance)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Printf("خطا در انتقال MySQL: حساب مبدا %d یافت نشد.\n", fromAccountID)
			return
		}
		log.Printf("خطا در خواندن موجودی حساب مبدا در MySQL: %v", err)
		return
	}

	if fromBalance < amount {
		fmt.Printf("خطا در انتقال MySQL: موجودی حساب %d (%f) کافی نیست برای %f.\n", fromAccountID, fromBalance, amount)
		return
	}

	_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance - ? WHERE id = ?;", amount, fromAccountID)
	if err != nil {
		log.Printf("خطا در کاهش موجودی حساب %d در MySQL: %v", fromAccountID, err)
		return
	}

	_, err = tx.ExecContext(ctx, "UPDATE accounts SET balance = balance + ? WHERE id = ?;", amount, toAccountID)
	if err != nil {
		log.Printf("خطا در افزایش موجودی حساب %d در MySQL: %v", toAccountID, err)
		return
	}

	err = tx.Commit()
	if err != nil {
		log.Printf("خطا در Commit تراکنش MySQL: %v", err)
		return
	}

	fmt.Printf("انتقال %f از حساب %d به حساب %d در MySQL با موفقیت انجام شد.\n", amount, fromAccountID, toAccountID)
}

func getAccountBalancesPG(db *sql.DB) {
	fmt.Println("\nموجودی حساب‌ها در PostgreSQL:")
	rows, err := db.Query("SELECT id, balance FROM accounts ORDER BY id;")
	if err != nil {
		log.Printf("خطا در کوئری موجودی حساب‌ها در PostgreSQL: %v", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var acc Account
		if err := rows.Scan(&acc.ID, &acc.Balance); err != nil {
			log.Printf("خطا در اسکن موجودی حساب در PostgreSQL: %v", err)
			continue
		}
		fmt.Printf("حساب ID: %d, موجودی: %f\n", acc.ID, acc.Balance)
	}
}

func getAccountBalancesMySQL(db *sql.DB) {
	fmt.Println("\nموجودی حساب‌ها در MySQL:")
	rows, err := db.Query("SELECT id, balance FROM accounts ORDER BY id;")
	if err != nil {
		log.Printf("خطا در کوئری موجودی حساب‌ها در MySQL: %v", err)
		return
	}
	defer rows.Close()

	for rows.Next() {
		var acc Account
		if err := rows.Scan(&acc.ID, &acc.Balance); err != nil {
			log.Printf("خطا در اسکن موجودی حساب در MySQL: %v", err)
			continue
		}
		fmt.Printf("حساب ID: %d, موجودی: %f\n", acc.ID, acc.Balance)
	}
}

در این مثال، از FOR UPDATE در کوئری SELECT استفاده شده است تا ردیف‌های مربوط به حساب‌ها قفل شوند و از "شرایط مسابقه" (Race Conditions) در محیط‌های همروند جلوگیری شود. این تضمین می‌کند که در طول تراکنش، هیچ تراکنش دیگری نمی‌تواند این ردیف‌ها را تغییر دهد.

بررسی ORMها و Query Builderها در Go

همانطور که قبلاً اشاره شد، database/sql یک API سطح پایین و انعطاف‌پذیر ارائه می‌دهد. با این حال، برای پروژه‌های بزرگ‌تر یا زمانی که نیاز به توسعه سریع‌تر دارید، ORMها (Object-Relational Mappers) و Query Builderها می‌توانند بسیار مفید باشند. آن‌ها به شما اجازه می‌دهند تا با پایگاه داده با استفاده از اشیاء و متدهای زبان Go کار کنید، و نیاز به نوشتن SQL خام را کاهش می‌دهند.

GORM: یک ORM محبوب Go

GORM یکی از کامل‌ترین و محبوب‌ترین ORMها در اکوسیستم Go است. این ORM طیف وسیعی از قابلیت‌ها از جمله مدل‌سازی داده‌ها، عملیات CRUD، تراکنش‌ها، Migrations، hooks و پلاگین‌ها را ارائه می‌دهد.

چرا GORM؟

  • سهولت استفاده: API ساده و شهودی برای عملیات پایگاه داده.
  • مدل‌سازی آسان: تعریف مدل‌ها با استفاده از structها و تگ‌های Go.
  • CRUD جامع: پشتیبانی کامل از Create, Read, Update, Delete.
  • پشتیبانی از روابط: One-to-One, One-to-Many, Many-to-Many.
  • Migrate خودکار: قابلیت Migration خودکار schema پایگاه داده بر اساس مدل‌های شما.
  • Hooks و Callbacks: امکان اجرای کد قبل یا بعد از عملیات‌های پایگاه داده.
  • پشتیبانی از درایورهای مختلف: PostgreSQL, MySQL, SQLite, SQL Server.

نصب GORM


go get gorm.io/gorm
go get gorm.io/driver/postgres // برای PostgreSQL
go get gorm.io/driver/mysql    // برای MySQL

اتصال GORM به PostgreSQL و MySQL


package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// User model for GORM
type UserGORM struct {
	gorm.Model // Adds ID, CreatedAt, UpdatedAt, DeletedAt
	Name       string `gorm:"type:varchar(100);not null"`
	Email      string `gorm:"type:varchar(100);unique;not null"`
}

func main() {
	// PostgreSQL connection with GORM
	pgDSN := "host=localhost user=postgres password=root dbname=mydatabase port=5432 sslmode=disable TimeZone=Asia/Tehran"
	dbPG, err := gorm.Open(postgres.Open(pgDSN), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // Show SQL queries in logs
	})
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL با GORM: %v", err)
	}
	fmt.Println("با موفقیت به PostgreSQL با GORM متصل شدید!")

	// Migrate the schema (create table if not exists)
	err = dbPG.AutoMigrate(&UserGORM{})
	if err != nil {
		log.Fatalf("خطا در Migration جدول UserGORM در PostgreSQL: %v", err)
	}
	fmt.Println("Migration برای جدول UserGORM در PostgreSQL انجام شد.")

	// MySQL connection with GORM
	mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"
	dbMySQL, err := gorm.Open(mysql.Open(mysqlDSN), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // Show SQL queries in logs
	})
	if err != nil {
		log.Fatalf("خطا در اتصال به MySQL با GORM: %v", err)
	}
	fmt.Println("با موفقیت به MySQL با GORM متصل شدید!")

	err = dbMySQL.AutoMigrate(&UserGORM{})
	if err != nil {
		log.Fatalf("خطا در Migration جدول UserGORM در MySQL: %v", err)
	}
	fmt.Println("Migration برای جدول UserGORM در MySQL انجام شد.")

	// CRUD operations with GORM on PostgreSQL
	fmt.Println("\n--- عملیات GORM روی PostgreSQL ---")
	performGormCRUD(dbPG)

	// CRUD operations with GORM on MySQL
	fmt.Println("\n--- عملیات GORM روی MySQL ---")
	performGormCRUD(dbMySQL)
}

func performGormCRUD(db *gorm.DB) {
	// Create
	fmt.Println("ایجاد کاربر جدید...")
	user1 := UserGORM{Name: "محمد", Email: fmt.Sprintf("mohammad%d@example.com", time.Now().UnixNano())}
	result := db.Create(&user1)
	if result.Error != nil {
		log.Printf("خطا در ایجاد کاربر: %v", result.Error)
		return
	}
	fmt.Printf("کاربر جدید ایجاد شد: ID: %d, Name: %s\n", user1.ID, user1.Name)

	user2 := UserGORM{Name: "فاطمه", Email: fmt.Sprintf("fatemeh%d@example.com", time.Now().UnixNano())}
	db.Create(&user2)

	// Read all users
	fmt.Println("\nخواندن تمام کاربران:")
	var users []UserGORM
	db.Find(&users)
	for _, user := range users {
		fmt.Printf("ID: %d, Name: %s, Email: %s\n", user.ID, user.Name, user.Email)
	}

	// Read a single user by ID
	fmt.Println("\nخواندن کاربر بر اساس ID (اولین کاربر):")
	var retrievedUser UserGORM
	db.First(&retrievedUser, user1.ID)
	fmt.Printf("کاربر بازیابی شده: ID: %d, Name: %s, Email: %s\n", retrievedUser.ID, retrievedUser.Name, retrievedUser.Email)

	// Update user
	fmt.Println("\nبه‌روزرسانی کاربر:")
	db.Model(&retrievedUser).Update("Name", "محمد رضا")
	db.Model(&retrievedUser).Updates(UserGORM{Email: "mohammad.reza.updated@example.com"}) // Bulk update for specific fields
	fmt.Printf("کاربر به‌روزرسانی شده: ID: %d, Name: %s, Email: %s\n", retrievedUser.ID, retrievedUser.Name, retrievedUser.Email)

	// Re-read to confirm update
	db.First(&retrievedUser, user1.ID)
	fmt.Printf("کاربر بعد از به‌روزرسانی (بازخوانی شده): ID: %d, Name: %s, Email: %s\n", retrievedUser.ID, retrievedUser.Name, retrievedUser.Email)


	// Delete user
	fmt.Println("\nحذف کاربر:")
	db.Delete(&user2)
	fmt.Printf("کاربر با ID %d حذف شد.\n", user2.ID)

	// Check if user still exists
	var deletedUser UserGORM
	result = db.First(&deletedUser, user2.ID)
	if result.Error != nil {
		if result.Error == gorm.ErrRecordNotFound {
			fmt.Printf("کاربر با ID %d یافت نشد (با موفقیت حذف شد).\n", user2.ID)
		} else {
			log.Printf("خطا در بررسی کاربر حذف شده: %v", result.Error)
		}
	} else {
		fmt.Printf("کاربر با ID %d هنوز وجود دارد (حذف ناموفق).\n", user2.ID)
	}
}

در این مثال، gorm.Model فیلدهای ID، CreatedAt، UpdatedAt و DeletedAt را به صورت خودکار اضافه می‌کند (برای Soft Delete). تگ‌های gorm:"..." به GORM کمک می‌کنند تا schema را به درستی نگاشت کند. AutoMigrate به صورت خودکار جداول را ایجاد یا به‌روزرسانی می‌کند. GORM به شما اجازه می‌دهد تا عملیات CRUD را با متدهایی مانند Create، Find، First، Updates و Delete انجام دهید.

محدودیت‌های GORM

با وجود مزایای فراوان، GORM (مانند سایر ORMها) دارای معایبی نیز هست:

  • پیچیدگی پنهان: گاهی اوقات ORMها کوئری‌های SQL ناکارآمد تولید می‌کنند که دیباگ کردن و بهینه‌سازی آن‌ها دشوار است.
  • یادگیری منحنی: یادگیری GORM API و "روش GORM" ممکن است زمان‌بر باشد.
  • کنترل کمتر: برای کوئری‌های بسیار پیچیده و خاص، ممکن است نیاز به نوشتن SQL خام از طریق متدهای Raw یا Exec در GORM داشته باشید.
  • انتزاع بیش از حد: ممکن است شما را از درک عمیق‌تر نحوه کار پایگاه داده و SQL دور کند.

SQLx: گسترش دهنده database/sql

SQLx یک پکیج شخص ثالث است که بر روی database/sql ساخته شده و قابلیت‌های آن را بهبود می‌بخشد، به خصوص در زمینه نگاشت نتایج کوئری به ساختارهای Go. SQLx یک ORM کامل نیست، بلکه یک "Query Builder" و "Mapper" است که شکاف بین database/sql خالص و یک ORM کامل مانند GORM را پر می‌کند.

چرا SQLx؟

  • کاهش کد boilerplate: اسکن کردن نتایج کوئری به structها بسیار آسان‌تر است.
  • پشتیبانی از نام‌گذاری ستون‌ها: می‌توانید با استفاده از تگ db:"column_name"، فیلدهای struct را به ستون‌های پایگاه داده نگاشت کنید.
  • پشتیبانی از In-Clause: به راحتی می‌توانید کوئری‌هایی با IN (...) بسازید.
  • عدم پنهان‌سازی SQL: شما همچنان کوئری‌های SQL خود را می‌نویسید، اما SQLx به شما کمک می‌کند آن‌ها را راحت‌تر اجرا کرده و نتایج را نگاشت کنید.
  • نزدیکی به database/sql: اگر قبلاً با database/sql کار کرده‌اید، انتقال به SQLx بسیار آسان است.

نصب SQLx


go get github.com/jmoiron/sqlx

مثال CRUD با SQLx


package main

import (
	"fmt"
	"log"
	"time"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
	_ "github.com/go-sql-driver/mysql"
)

// Product model for SQLx
type Product struct {
	ID    int       `db:"id"`
	Name  string    `db:"name"`
	Price float64   `db:"price"`
	CreatedAt time.Time `db:"created_at"`
}

func main() {
	// PostgreSQL connection with SQLx
	pgDSN := "host=localhost port=5432 user=postgres password=root dbname=mydatabase sslmode=disable"
	dbPG, err := sqlx.Connect("postgres", pgDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به PostgreSQL با SQLx: %v", err)
	}
	defer dbPG.Close()
	fmt.Println("با موفقیت به PostgreSQL با SQLx متصل شدید!")

	// Create table for SQLx example
	createProductsTablePG(dbPG)

	// MySQL connection with SQLx
	mysqlDSN := "root:root@tcp(127.0.0.1:3306)/mydatabase?charset=utf8mb4&parseTime=True&loc=Local"
	dbMySQL, err := sqlx.Connect("mysql", mysqlDSN)
	if err != nil {
		log.Fatalf("خطا در اتصال به MySQL با SQLx: %v", err)
	}
	defer dbMySQL.Close()
	fmt.Println("با موفقیت به MySQL با SQLx متصل شدید!")

	// Create table for SQLx example
	createProductsTableMySQL(dbMySQL)

	// CRUD operations with SQLx on PostgreSQL
	fmt.Println("\n--- عملیات SQLx روی PostgreSQL ---")
	performSqlxCRUD(dbPG, "postgres")

	// CRUD operations with SQLx on MySQL
	fmt.Println("\n--- عملیات SQLx روی MySQL ---")
	performSqlxCRUD(dbMySQL, "mysql")
}

func createProductsTablePG(db *sqlx.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS products (
		id SERIAL PRIMARY KEY,
		name VARCHAR(100) NOT NULL,
		price NUMERIC(10, 2) NOT NULL,
		created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول products در PostgreSQL: %v", err)
	}
	fmt.Println("جدول products در PostgreSQL با موفقیت ایجاد یا موجود بود.")
}

func createProductsTableMySQL(db *sqlx.DB) {
	query := `
	CREATE TABLE IF NOT EXISTS products (
		id INT AUTO_INCREMENT PRIMARY KEY,
		name VARCHAR(100) NOT NULL,
		price DECIMAL(10, 2) NOT NULL,
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
	);`
	_, err := db.Exec(query)
	if err != nil {
		log.Fatalf("خطا در ایجاد جدول products در MySQL: %v", err)
	}
	fmt.Println("جدول products در MySQL با موفقیت ایجاد یا موجود بود.")
}

func performSqlxCRUD(db *sqlx.DB, driverName string) {
	// Insert
	fmt.Println("درج محصول جدید...")
	productName := fmt.Sprintf("کتاب %s %d", driverName, time.Now().UnixNano())
	price := 25.99
	query := `INSERT INTO products (name, price) VALUES (:name, :price)`
	if driverName == "postgres" {
		query = `INSERT INTO products (name, price) VALUES ($1, $2)`
	}

	var result sql.Result
	var err error

	if driverName == "postgres" {
		result, err = db.Exec(query, productName, price)
	} else {
		// Use NamedExec for MySQL to benefit from struct mapping
		result, err = db.NamedExec(query, map[string]interface{}{"name": productName, "price": price})
	}
	
	if err != nil {
		log.Printf("خطا در درج محصول: %v", err)
		return
	}
	var insertedID int64
	if driverName == "postgres" {
		// For Postgres, need to QueryRow to get ID, or use RETURNING in the query for better experience.
		// For simplicity, we assume ID is not needed right after insert for this example,
		// or would use a specific RETURNING query if available.
		// A more robust insert in PG with sqlx for returning ID would be:
		// var id int
		// err = db.QueryRowx(`INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id`, productName, price).Scan(&id)
		insertedID = -1 // Placeholder as getting ID with simple Exec is harder for PG
	} else {
		insertedID, _ = result.LastInsertId()
	}

	fmt.Printf("محصول جدید درج شد: Name: %s, ID (if available): %d\n", productName, insertedID)

	// Select multiple
	fmt.Println("\nخواندن تمام محصولات:")
	var products []Product
	err = db.Select(&products, `SELECT id, name, price, created_at FROM products ORDER BY id DESC;`)
	if err != nil {
		log.Printf("خطا در خواندن محصولات: %v", err)
		return
	}
	for _, p := range products {
		fmt.Printf("ID: %d, Name: %s, Price: %.2f, CreatedAt: %s\n", p.ID, p.Name, p.Price, p.CreatedAt.Format("2006-01-02 15:04:05"))
	}

	// Select one
	fmt.Println("\nخواندن یک محصول بر اساس نام:")
	var singleProduct Product
	err = db.Get(&singleProduct, `SELECT id, name, price, created_at FROM products WHERE name = $1;`, productName)
	if driverName == "mysql" { // adjust placeholder for mysql
		err = db.Get(&singleProduct, `SELECT id, name, price, created_at FROM products WHERE name = ?;`, productName)
	}
	
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Printf("محصول با نام '%s' یافت نشد.\n", productName)
		} else {
			log.Printf("خطا در خواندن محصول تکی: %v", err)
		}
		return
	}
	fmt.Printf("محصول بازیابی شده: ID: %d, Name: %s, Price: %.2f\n", singleProduct.ID, singleProduct.Name, singleProduct.Price)

	// Update
	fmt.Println("\nبه‌روزرسانی قیمت محصول:")
	newPrice := 29.99
	updateQuery := `UPDATE products SET price = $1 WHERE id = $2;`
	if driverName == "mysql" {
		updateQuery = `UPDATE products SET price = ? WHERE id = ?;`
	}
	
	res, err := db.Exec(updateQuery, newPrice, singleProduct.ID)
	if err != nil {
		log.Printf("خطا در به‌روزرسانی محصول: %v", err)
		return
	}
	rowsAffected, _ := res.RowsAffected()
	fmt.Printf("محصول با ID %d به‌روزرسانی شد. ردیف‌های تحت تأثیر: %d\n", singleProduct.ID, rowsAffected)

	// Delete
	fmt.Println("\nحذف محصول:")
	deleteQuery := `DELETE FROM products WHERE id = $1;`
	if driverName == "mysql" {
		deleteQuery = `DELETE FROM products WHERE id = ?;`
	}
	
	res, err = db.Exec(deleteQuery, singleProduct.ID)
	if err != nil {
		log.Printf("خطا در حذف محصول: %v", err)
		return
	}
	rowsAffected, _ = res.RowsAffected()
	fmt.Printf("محصول با ID %d حذف شد. ردیف‌های تحت تأثیر: %d\n", singleProduct.ID, rowsAffected)
}

در این مثال:

  • sqlx.Connect یک اتصال ایجاد می‌کند و db.Ping() را اجرا می‌کند.
  • تگ db:"column_name" به SQLx می‌گوید چگونه فیلدهای struct را به ستون‌های پایگاه داده نگاشت کند.
  • db.Select(&products, ...) تمام نتایج را به یک اسلایس از structها اسکن می‌کند.
  • db.Get(&singleProduct, ...) یک ردیف را به یک struct اسکن می‌کند.
  • db.NamedExec(query, struct_or_map) به شما اجازه می‌دهد تا پارامترهای نام‌گذاری شده را در کوئری‌های خود استفاده کنید، که SQLx آن‌ها را به پارامترهای موقعیت‌یابی (positional parameters) تبدیل می‌کند (به خصوص برای MySQL مفید است).

تفاوت با GORM

  • انتزاع کمتر: SQLx به شما اجازه می‌دهد SQL خام بنویسید و سپس به طور کارآمدتری نتایج را به structها نگاشت کنید. GORM سعی می‌کند SQL را از شما پنهان کند.
  • کنترل بیشتر: از آنجا که شما SQL را می‌نویسید، کنترل بیشتری بر کوئری‌ها و بهینه‌سازی‌ها دارید.
  • بدون ORM کامل: SQLx قابلیت‌های ORM کاملی مانند AutoMigrate، روابط پیچیده یا hooks را ندارد.
  • برای کدام سناریو؟ SQLx برای پروژه‌هایی که نیاز به عملکرد بالا و کنترل دقیق بر کوئری‌ها دارند، اما همچنان می‌خواهند از مزایای نگاشت خودکار به structها بهره ببرند، انتخاب خوبی است. GORM برای توسعه سریع‌تر و پروژه‌هایی که پیچیدگی SQL کمتری دارند یا مایلند از ORM API بهره ببرند، مناسب‌تر است.

بهینه‌سازی و بهترین شیوه‌ها (Best Practices)

برای ساخت برنامه‌های Go که به طور کارآمد و قابل اعتماد با پایگاه داده تعامل دارند، رعایت بهترین شیوه‌ها و تکنیک‌های بهینه‌سازی ضروری است.

Connection Pooling و تنظیمات بهینه

همانطور که قبلاً ذکر شد، sql.DB یک Connection Pool را مدیریت می‌کند. تنظیم صحیح پارامترهای آن برای عملکرد بسیار مهم است:

  • SetMaxOpenConns: این مقدار نباید بیش از حد بالا باشد، زیرا هر اتصال یک سربار (overhead) روی پایگاه داده و اپلیکیشن ایجاد می‌کند. این عدد باید با تعداد حداکثر اتصالات پایگاه داده شما و تعداد Goroutineهای همزمان که نیاز به اتصال دارند، متناسب باشد.
  • SetMaxIdleConns: این مقدار باید به اندازه کافی بالا باشد تا اتصالات آماده برای استفاده مجدد در دسترس باشند. اما اگر خیلی زیاد باشد، ممکن است پایگاه داده را با اتصالات بیکار زیادی پر کند. یک نقطه شروع خوب می‌تواند SetMaxIdleConns کمتر یا مساوی با SetMaxOpenConns باشد.
  • SetConnMaxLifetime: این پارامتر برای جلوگیری از استفاده از اتصالات "کهنه" که ممکن است توسط سرور پایگاه داده بسته شده باشند (مثلاً به دلیل Timeout) مهم است. تنظیم آن به یک مقدار معقول (مثلاً 5-10 دقیقه) می‌تواند به حفظ پایداری کمک کند.

مانیتورینگ (Monitoring) اتصالات و تنظیمات بهترین راه برای پیدا کردن مقادیر بهینه است.

استفاده از PreparedStatementها برای امنیت و کارایی

همیشه از پارامترهای با جای‌گذاری (Parameterized Queries) استفاده کنید (مثلاً $1 در PostgreSQL یا ? در MySQL) و هرگز مقادیر را مستقیماً به رشته SQL نچسبانید. این کار دو مزیت اصلی دارد:

  • امنیت (SQL Injection): از حملات SQL Injection جلوگیری می‌کند، زیرا مقادیر به عنوان داده در نظر گرفته می‌شوند، نه کد اجرایی.
  • کارایی: پایگاه داده می‌تواند PreparedStatementها را کامپایل کرده و مجدداً از آن‌ها استفاده کند، که زمان پردازش کوئری را در هر اجرای مجدد کاهش می‌دهد.

توابع db.Exec()، db.Query() و db.QueryRow() به صورت پیش‌فرض از PreparedStatementها استفاده می‌کنند.

مدیریت خطا (Error Handling) جامع

هرگز خطاهای برگشتی از عملیات‌های پایگاه داده را نادیده نگیرید. همیشه آن‌ها را بررسی کرده و به درستی مدیریت کنید. به خصوص به sql.ErrNoRows برای کوئری‌هایی که ممکن است هیچ نتیجه‌ای برنگردانند، توجه کنید. در صورت بروز خطا، Rollback کردن تراکنش‌ها و Log-گیری از خطاها بسیار مهم است.

Log-گیری از کوئری‌ها و خطاها

Log-گیری دقیق از کوئری‌های اجرا شده، زمان اجرا، و هرگونه خطا برای دیباگ کردن، بهینه‌سازی عملکرد و عیب‌یابی در محیط تولید بسیار ضروری است. می‌توانید از پکیج‌های Log-گیری مانند log استاندارد Go یا پکیج‌های پیشرفته‌تر مانند logrus یا Zap استفاده کنید. ORMهایی مانند GORM نیز دارای قابلیت Log-گیری داخلی هستند که می‌توانید آن‌ها را پیکربندی کنید.

طراحی Schema مناسب

طراحی صحیح schema پایگاه داده (شامل تعریف جداول، انواع داده‌ها، کلیدهای اصلی و خارجی، ایندکس‌ها و محدودیت‌ها) اساسی‌ترین قدم برای عملکرد خوب و پایداری است. ایندکس‌گذاری مناسب برای ستون‌هایی که در WHERE clauses، JOINs و ORDER BY استفاده می‌شوند، می‌تواند تفاوت چشمگیری در عملکرد کوئری‌ها ایجاد کند.

امنیت (SQL Injection, DSN management)

  • SQL Injection: همانطور که ذکر شد، همیشه از پارامترهای با جای‌گذاری استفاده کنید.
  • DSN Management: اطلاعات حساس DSN (نام کاربری و رمز عبور) را هرگز به صورت مستقیم در کد خود hardcode نکنید. از متغیرهای محیطی، Secret Management Systems (مانند Vault) یا فایل‌های پیکربندی امن استفاده کنید.
  • کمترین امتیاز (Least Privilege): کاربران پایگاه داده‌ای که برنامه شما از آن‌ها استفاده می‌کند، باید حداقل امتیازات لازم برای انجام عملیات‌های مورد نیاز را داشته باشند. به عنوان مثال، یک کاربر فقط برای خواندن، نباید قابلیت نوشتن داشته باشد.

پیمایش (Pagination) موثر

برای مجموعه داده‌های بزرگ، هرگز تمام ردیف‌ها را در یک کوئری بارگذاری نکنید. از تکنیک‌های پیمایش (Pagination) مانند OFFSET و LIMIT یا پیمایش مبتنی بر مکان‌نما (Cursor-based Pagination) برای بارگذاری بخش‌های کوچکتر از داده استفاده کنید. پیمایش مبتنی بر مکان‌نما معمولاً برای مجموعه داده‌های بسیار بزرگ کارایی بهتری دارد.


// Example: Offset-based pagination
query := `SELECT id, name, email FROM users ORDER BY id LIMIT $1 OFFSET $2;` // PostgreSQL
rows, err := db.Query(query, limit, offset)
// ...

استفاده از Context برای لغو کوئری‌ها و مهلت زمانی

پکیج context در Go برای مدیریت مهلت‌های زمانی (Timeouts)، لغو عملیات‌ها و ارسال مقادیر در زنجیره فراخوانی توابع بسیار مفید است. توابع پایگاه داده در database/sql (مانند ExecContext، QueryContext و QueryRowContext) از context.Context پشتیبانی می‌کنند. استفاده از آن به شما امکان می‌دهد تا یک کوئری طولانی مدت را در صورت عدم پاسخگویی پایگاه داده، لغو کنید و از مسدود شدن Goroutineها جلوگیری کنید.


ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Release resources associated with the context

_, err := db.ExecContext(ctx, "INSERT INTO large_table (data) VALUES ($1);", bigData)
if err != nil {
    // Check if err is due to context.DeadlineExceeded or context.Canceled
    if errors.Is(err, context.DeadlineExceeded) {
        log.Println("کوئری پایگاه داده به دلیل Timeout لغو شد.")
    } else {
        log.Printf("خطا در اجرای کوئری: %v", err)
    }
}

Migrations پایگاه داده

با تکامل اپلیکیشن شما، schema پایگاه داده نیز نیاز به تغییر خواهد داشت. مدیریت این تغییرات به صورت کنترل شده و ورژن‌بندی شده، به عنوان Migrations شناخته می‌شود. استفاده از ابزارهای Migration (مانند migrate، goose یا قابلیت‌های Migration داخلی GORM) به شما کمک می‌کند تا تغییرات schema را به طور خودکار و ایمن اعمال کنید و از ناهماهنگی بین کد و پایگاه داده جلوگیری کنید.

نتیجه‌گیری

در این مقاله جامع، ما به تفصیل به بررسی نحوه اتصال و تعامل با پایگاه‌های داده PostgreSQL و MySQL در Go پرداختیم. از پکیج بنیادین database/sql به عنوان ستون فقرات ارتباطات پایگاه داده شروع کردیم و دیدیم که چگونه با استفاده از درایورهای اختصاصی مانند github.com/lib/pq و github.com/go-sql-driver/mysql می‌توانیم به این پایگاه‌های داده محبوب متصل شویم.

ما عملیات‌های اساسی CRUD را با مثال‌های کد کامل برای هر دو پایگاه داده نشان دادیم و بر اهمیت مدیریت Connection Pool و استفاده از PreparedStatementها برای امنیت و کارایی تأکید کردیم. همچنین، پیچیدگی‌های مدیریت تراکنش‌ها را تشریح کردیم و با ارائه یک مثال عملی، نحوه تضمین ACID properties را در عملیات‌های مالی نشان دادیم.

در ادامه، به بررسی ORMها و Query Builderهای محبوب Go پرداختیم. GORM، به عنوان یک ORM کامل، توسعه سریع‌تر و انتزاع بالاتری را فراهم می‌کند و به شما اجازه می‌دهد با مدل‌های Go به جای SQL خام کار کنید. در مقابل، SQLx، با ارائه قابلیت‌های نگاشت بهبودیافته روی database/sql، رویکردی بینابینی ارائه می‌دهد که کنترل بیشتری بر SQL به شما می‌دهد و در عین حال کد boilerplate را کاهش می‌دهد.

در نهایت، به مجموعه‌ای از بهترین شیوه‌ها و نکات بهینه‌سازی پرداختیم که برای ساخت برنامه‌های پایگاه داده‌ای قوی، مقیاس‌پذیر و امن در Go حیاتی هستند. از تنظیمات Connection Pool و مدیریت خطا گرفته تا استفاده از Context و ابزارهای Migration، هر یک از این جنبه‌ها به پایداری و عملکرد اپلیکیشن شما کمک می‌کنند.

انتخاب ابزار مناسب (native database/sql vs. ORM)

انتخاب بین استفاده مستقیم از database/sql، یک Query Builder مانند SQLx، یا یک ORM کامل مانند GORM به عوامل مختلفی بستگی دارد:

  • database/sql خالص: بهترین گزینه برای پروژه‌هایی است که نیاز به کنترل کامل بر SQL دارند، یا برای بخش‌هایی از کد که عملکرد فوق‌العاده حیاتی است و پیچیدگی SQL زیاد است. یادگیری و نوشتن کد بیشتر مورد نیاز است.
  • SQLx: یک نقطه میانی عالی. اگر می‌خواهید SQL خود را بنویسید اما از نگاشت آسان به structها بهره ببرید و کدboilerplate کمتری داشته باشید، SQLx انتخابی ایده‌آل است.
  • GORM: برای توسعه سریع، پروژه‌هایی با مدل‌های داده‌ای پیچیده و نیاز به ویژگی‌های ORM مانند AutoMigrate و روابط، GORM می‌تواند بهره‌وری را به شدت افزایش دهد. با این حال، باید آماده پذیرش میزان مشخصی از "Magic" و از دست دادن کنترل دقیق بر SQL باشید.

در عمل، بسیاری از پروژه‌های بزرگ ترکیبی از این رویکردها را به کار می‌گیرند؛ استفاده از ORM برای عملیات‌های رایج و سریع و بازگشت به SQL خالص یا SQLx برای کوئری‌های پیچیده و بهینه‌شده.

مسیرهای آینده

دنیای توسعه پایگاه داده در Go همواره در حال پیشرفت است. با پیشرفت‌های آینده در زبان Go و اکوسیستم آن، انتظار می‌رود ابزارهای قدرتمندتر و کارآمدتری برای کار با پایگاه داده‌ها ظاهر شوند. تمرکز بر معماری‌های Clean Architecture و Domain-Driven Design (DDD) در کنار ابزارهای پایگاه داده، به شما کمک می‌کند تا سیستم‌هایی بسازید که نه تنها کارآمد هستند، بلکه قابل نگهداری و گسترش نیز باشند.

با تسلط بر مفاهیم و تکنیک‌های پوشش داده شده در این مقاله، شما مجهز خواهید بود تا برنامه‌های Go قوی و کارآمدی بسازید که به بهترین شکل با پایگاه داده‌های رابطه‌ای تعامل دارند.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان