اتصال به PostgreSQL
خب، حالا که database جدید greenlight راهاندازی شده، بیایید ببینیم چطور از application نوشتهشده با Go به آن وصل شویم.
همانطور که احتمالا از Let’s Go یادتان هست، برای کار با یک SQL database باید از یک database driver استفاده کنیم تا به عنوان «واسط» بین Go و خود database عمل کند. میتوانید فهرست driverهای موجود برای PostgreSQL را در Go wiki ببینید، اما برای پروژه خودمان سراغ پکیج محبوب، reliable و جاافتاده pq میرویم.
اگر همراه کتاب کدنویسی میکنید، از go get استفاده کنید تا آخرین release از شاخه v1.N.N مربوط به pq را به این شکل دانلود کنید:
$ go get github.com/lib/pq@v1 go: downloading github.com/lib/pq v1.10.9 go get: added github.com/lib/pq v1.10.9
برای اتصال به database به یک data source name یا DSN هم نیاز داریم، که اساسا stringای شامل پارامترهای لازم connection است. format دقیق DSN به database driverای بستگی دارد که استفاده میکنید و باید در مستندات همان driver توضیح داده شده باشد، اما هنگام استفاده از pq باید بتوانید با DSN زیر به database local مربوط به greenlight به عنوان user مربوط به greenlight وصل شوید:
postgres://greenlight:pa55word@localhost/greenlight
ایجاد connection pool
کدی که برای اتصال application نوشتهشده با Go به database مربوط به greenlight استفاده میکنیم، تقریبا دقیقا همان چیزی است که در کتاب اول Let’s Go داشتیم. پس روی جزئیات زیاد مکث نمیکنیم و امیدوارم همه اینها برایتان آشنا باشد.
در سطح کلی:
میخواهیم DSN در runtime قابل پیکربندی باشد، پس به جای hard-code کردن، آن را با استفاده از یک command-line flag به application پاس میدهیم. برای سادگی در زمان development، از DSN بالا به عنوان مقدار پیشفرض flag استفاده میکنیم.
در فایل
cmd/api/main.goیک helper function جدید به نامopenDB()میسازیم. در این helper از function مربوط بهsql.Open()برای ایجاد یک connection pool جدید از نوعsql.DBاستفاده میکنیم؛ سپس چون connectionها به database به صورت lazy و فقط وقتی برای اولین بار لازم باشند ایجاد میشوند، باید از متدdb.PingContext()هم استفاده کنیم تا واقعا یک connection ایجاد شود و تایید کنیم همه چیز درست setup شده است.
بیایید به فایل cmd/api/main.go برگردیم و آن را به این شکل بهروزرسانی کنیم:
package main import ( "context" // New import "database/sql" // New import "flag" "fmt" "log/slog" "net/http" "os" "time" // Import the pq driver so that it can register itself with the database/sql // package. Note that we alias this import to the blank identifier, to stop the Go // compiler complaining that the package isn't being used. _ "github.com/lib/pq" ) const version = "1.0.0" // Add a db struct field to hold the configuration settings for our database connection // pool. For now this only holds the DSN, which we will read in from a command-line flag. type config struct { port int env string db struct { dsn string } } type application struct { config config logger *slog.Logger } func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") // Read the DSN value from the db-dsn command-line flag into the config struct. We // default to using our development DSN if no flag is provided. flag.StringVar(&cfg.db.dsn, "db-dsn", "postgres://greenlight:pa55word@localhost/greenlight", "PostgreSQL DSN") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) // Call the openDB() helper function (see below) to create the connection pool, // passing in the config struct as an argument. If this returns an error, we log // it and exit the application immediately. db, err := openDB(cfg) if err != nil { logger.Error(err.Error()) os.Exit(1) } // Defer a call to db.Close() so that the connection pool is closed before the // main() function exits. defer db.Close() // Also log a message to say that the connection pool has been successfully // established. logger.Info("database connection pool established") app := &application{ config: cfg, logger: logger, } srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: time.Minute, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError), } logger.Info("starting server", "addr", srv.Addr, "env", cfg.env) // Because the err variable is now already declared in the code above, we need // to use the = operator here, instead of the := operator. err = srv.ListenAndServe() logger.Error(err.Error()) os.Exit(1) } // The openDB() function returns a sql.DB connection pool. func openDB(cfg config) (*sql.DB, error) { // Use sql.Open() to create an empty connection pool, using the DSN from the config // struct. db, err := sql.Open("postgres", cfg.db.dsn) if err != nil { return nil, err } // Create a context with a 5-second timeout deadline. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Use PingContext() to establish a new connection to the database, passing in the // context we created above as a parameter. If the connection couldn't be // established successfully within the 5-second deadline, then this will return an // error. If we get this error, or any other, we close the connection pool and // return the error. err = db.PingContext(ctx) if err != nil { db.Close() return nil, err } // Return the sql.DB connection pool. return db, nil }
وقتی فایل cmd/api/main.go بهروزرسانی شد، application را دوباره اجرا کنید. حالا باید هنگام startup یک log message ببینید که تایید میکند connection pool با موفقیت ایجاد شده است. شبیه این:
$ go run ./cmd/api time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established" time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development
جدا کردن DSN
در حال حاضر مقدار پیشفرض command-line flag برای DSN ما به صورت صریح در قالب یک string داخل فایل cmd/api/main.go قرار دارد.
با اینکه username و password داخل DSN فقط برای development database روی ماشین local شما هستند، بهتر است این اطلاعات داخل فایلهای پروژه hard-code نشده باشند؛ فایلهایی که ممکن است در آینده share یا distribute شوند.
پس چند قدم برمیداریم تا DSN را از کد پروژه جدا کنیم و به جای آن، به عنوان یک environment variable روی ماشین local شما ذخیره کنیم.
اگر همراه کتاب جلو میروید، با اضافه کردن خط زیر به یکی از فایلهای $HOME/.profile یا $HOME/.bashrc، یک environment variable جدید به نام GREENLIGHT_DB_DSN بسازید:
... export GREENLIGHT_DB_DSN='postgres://greenlight:pa55word@localhost/greenlight'
وقتی این کار انجام شد، باید کامپیوتر خود را reboot کنید، یا اگر الان راحت نیست، command مربوط به source را روی فایلی که تازه ویرایش کردهاید اجرا کنید تا تغییر اعمال شود. برای مثال:
$ source $HOME/.profile
در هر صورت، وقتی reboot کردید یا source را اجرا کردید، باید بتوانید با اجرای command مربوط به echo مقدار environment variable مربوط به GREENLIGHT_DB_DSN را در terminal ببینید. به این شکل:
$ echo $GREENLIGHT_DB_DSN postgres://greenlight:pa55word@localhost/greenlight
حالا فایل cmd/api/main.go را بهروزرسانی میکنیم تا با استفاده از function مربوط به os.Getenv() به environment variable دسترسی پیدا کند و آن را به عنوان مقدار پیشفرض command-line flag مربوط به DSN تنظیم کند.
در عمل نسبتا straightforward است:
package main ... func main() { var cfg config flag.IntVar(&cfg.port, "port", 4000, "API server port") flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)") // Use the value of the GREENLIGHT_DB_DSN environment variable as the default value // for our db-dsn command-line flag. flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN") flag.Parse() ... }
اگر حالا application را دوباره restart کنید، باید ببینید که درست compile میشود و مثل قبل کار میکند.
همچنین میتوانید هنگام اجرای application، flag مربوط به -help را مشخص کنید. این باید متن توضیحی و مقدارهای پیشفرض سه command-line flag ما را خروجی دهد، از جمله مقدار DSN که از environment variable خوانده شده است. شبیه این:
$ go run ./cmd/api -help
Usage of /tmp/go-build417842398/b001/exe/api:
-db-dsn string
PostgreSQL DSN (default "postgres://greenlight:pa55word@localhost/greenlight")
-env string
Environment (development|staging|production) (default "development")
-port int
API server port (default 4000)
اطلاعات تکمیلی
استفاده از DSN با psql
یک اثر جانبی خوب ذخیره کردن DSN در environment variable این است که میتوانید از آن برای اتصال آسان به database مربوط به greenlight به عنوان user مربوط به greenlight استفاده کنید، به جای اینکه هنگام اجرای psql همه optionهای connection را دستی مشخص کنید. به این شکل:
$ psql $GREENLIGHT_DB_DSN psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. greenlight=>