ایجاد استخر اتصال پایگاه داده
حالا که پایگاه داده MySQL کاملاً راهاندازی شده و یک درایور نصب کردهایم، گام طبیعی بعدی اتصال به پایگاه داده از برنامه وب ما است.
برای انجام این کار به تابع sql.Open() Go نیاز داریم، که آن را کمی مانند این استفاده میکنید:
// The sql.Open() function initializes a new sql.DB object, which is essentially a // pool of database connections. db, err := sql.Open("mysql", "web:pass@/snippetbox?parseTime=true") if err != nil { ... }
چند نکته در مورد این کد برای توضیح و تأکید وجود دارد:
پارامتر اول به
sql.Open()نام درایور است و پارامتر دوم نام منبع داده است (گاهی اوقات رشته اتصال یا DSN نیز نامیده میشود) که نحوه اتصال به پایگاه داده شما را توصیف میکند.فرمت نام منبع داده بستگی به پایگاه داده و درایور که استفاده میکنید دارد. به طور معمول، میتوانید اطلاعات و مثالها را در مستندات درایور خاص خود پیدا کنید. برای درایوری که استفاده میکنیم میتوانید آن مستندات را اینجا پیدا کنید.
بخش
parseTime=trueاز DSN بالا یک پارامتر خاص درایور است که به درایور ما دستور میدهد فیلدهای SQLTIMEوDATEرا به اشیاء Gotime.Timeتبدیل کند.تابع
sql.Open()یک شیءsql.DBبرمیگرداند. این یک اتصال پایگاه داده نیست — این یک استخر از اتصالات متعدد است. این یک تفاوت مهم برای درک است. Go اتصالات در این استخر را در صورت نیاز مدیریت میکند و به طور خودکار اتصالات به پایگاه داده را از طریق درایور باز و بسته میکند.استخر اتصال برای دسترسی همزمان (concurrent access) امن است، بنابراین میتوانید از آن در handlerهای برنامه وب به طور ایمن استفاده کنید.
استخر اتصال برای طولانیمدت در نظر گرفته شده است. در یک برنامه وب، طبیعی است که استخر اتصال را در تابع
main()خود مقداردهی اولیه کنید و سپس استخر را به handlerهای خود ارسال کنید. نبایدsql.Open()را در خود یک HTTP handler کوتاهمدت فراخوانی کنید — این اتلاف حافظه و منابع شبکه خواهد بود.
استفاده از آن در برنامه وب ما
بیایید نحوه استفاده از sql.Open() در عمل را ببینیم. فایل main.go خود را باز کنید و کد زیر را اضافه کنید:
package main import ( "database/sql" // New import "flag" "log/slog" "net/http" "os" _ "github.com/go-sql-driver/mysql" // New import ) ... func main() { addr := flag.String("addr", ":4000", "HTTP network address") // Define a new command-line flag for the MySQL DSN string. dsn := flag.String("dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) // To keep the main() function tidy I've put the code for creating a connection // pool into the separate openDB() function below. We pass openDB() the DSN // from the command-line flag. db, err := openDB(*dsn) if err != nil { logger.Error(err.Error()) os.Exit(1) } // We also defer a call to db.Close(), so that the connection pool is closed // before the main() function exits. defer db.Close() app := &application{ logger: logger, } logger.Info("starting server", "addr", *addr) // Because the err variable is now already declared in the code above, we need // to use the assignment operator = here, instead of the := 'declare and assign' // operator. err = http.ListenAndServe(*addr, app.routes()) logger.Error(err.Error()) os.Exit(1) } // The openDB() function wraps sql.Open() and returns a sql.DB connection pool // for a given DSN. func openDB(dsn string) (*sql.DB, error) { db, err := sql.Open("mysql", dsn) if err != nil { return nil, err } err = db.Ping() if err != nil { db.Close() return nil, err } return db, nil }
چند نکته جالب در مورد این کد وجود دارد:
توجه کنید که مسیر import برای درایور ما با یک زیرخط پیشوند شده است؟ این به این دلیل است که فایل
main.goما در واقع از هیچ چیزی در پکیجmysqlاستفاده نمیکند. بنابراین اگر سعی کنیم آن را به طور عادی import کنیم، کامپایلر Go خطا ایجاد میکند. با این حال، به اجرای تابعinit()درایور نیاز داریم تا بتواند خود را با پکیجdatabase/sqlثبت کند. ترفند دور زدن این، نام مستعار دادن نام پکیج به شناسه خالی است، همانطور که اینجا آمده است. این یک روش استاندارد برای بیشتر درایورهای SQL Go است.تابع
sql.Open()در واقع هیچ اتصالی ایجاد نمیکند، تنها کاری که انجام میدهد مقداردهی اولیه استخر برای استفاده آینده است. اتصالات واقعی به پایگاه داده به صورت تنبل برقرار میشوند، همانطور که برای اولین بار نیاز است. بنابراین برای تأیید اینکه همه چیز به درستی تنظیم شده است، باید از متدdb.Ping()برای ایجاد یک اتصال و بررسی هر خطایی استفاده کنیم. اگر خطایی وجود دارد،db.Close()را فراخوانی میکنیم تا استخر اتصال را ببندیم و خطا را برگردانیم.برگشت به تابع
main()، در این لحظه فراخوانیdefer db.Close()کمی اضافی است. برنامه ما فقط با یک سیگنال وقفه (یعنیCtrl+C) یا باos.Exit(1)خاتمه مییابد. در هر دو مورد، برنامه فوراً خارج میشود و توابع defer شده هرگز اجرا نمیشوند. اما اطمینان از بستن همیشه استخر اتصال عادت خوبی است که باید به آن عادت کنید، و میتواند در آینده مفید باشد اگر یک خاموشی آرام به برنامه خود اضافه کنید.
تست یک اتصال
مطمئن شوید که فایل ذخیره شده است، و سپس سعی کنید برنامه را اجرا کنید. اگر همه چیز طبق برنامه پیش رفته باشد، استخر اتصال باید ایجاد شود و متد db.Ping() باید بتواند یک اتصال بدون هیچ خطایی ایجاد کند. اگر همه چیز خوب باشد، باید پیام لاگ عادی starting server را مانند این ببینید:
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=INFO msg="starting server" addr=:4000
اگر برنامه راهاندازی نشود و یک پیام خطای "Access denied..." مانند زیر دریافت کنید، احتمالاً مشکل در DSN شما است. دوباره بررسی کنید که نام کاربری و رمز عبور صحیح هستند، که کاربران پایگاه داده شما مجوزهای مناسب دارند، و که نمونه MySQL شما از تنظیمات استاندارد استفاده میکند.
$ go run ./cmd/web time=2024-03-18T11:29:23.000+00:00 level=ERROR msg="Error 1045 (28000): Access denied for user 'web'@'localhost' (using password: YES)" exit status 1
مرتب کردن فایل go.mod
حالا که کد ما در واقع درایور github.com/go-sql-driver/mysql را import میکند، میتوانید دستور go mod tidy را اجرا کنید تا فایل go.mod خود را مرتب کنید و هر حاشیهنویسی غیرضروری // indirect را حذف کنید.
$ go mod tidy
پس از انجام این کار، فایل go.mod شما باید اکنون مانند زیر به نظر برسد — با github.com/go-sql-driver/mysql به عنوان یک وابستگی مستقیم فهرست شده و filippo.io/edwards25519 همچنان یک وابستگی غیرمستقیم باشد.
module snippetbox.alexedwards.net go 1.23.0 require github.com/go-sql-driver/mysql v1.8.1 require filippo.io/edwards25519 v1.1.0 // indirect