پرس و جوهای SQL تک رکوردی (Single Record SQL Queries)
در این بخش، ما متد SnippetModel.Get() را پیادهسازی خواهیم کرد تا یک رکورد واحد (Single Record) را بر اساس کلید اصلی (Primary Key) آن بازیابی کند.
برای این کار، از متد QueryRow() استفاده میکنیم که برای اجرای پرس و جوهای تک رکوردی (Single Record Queries) طراحی شده است.
برای انجام این کار، باید پرسوجوی SQL (SQL Query) زیر را در پایگاه داده اجرا کنیم:
SELECT id, title, content, created, expires FROM snippets WHERE expires > UTC_TIMESTAMP() AND id = ?
از آنجا که جدول snippets ما از ستون id به عنوان کلید اصلی (Primary Key) خود استفاده میکند، این پرسوجو همیشه دقیقاً یک سطر پایگاه داده (یا هیچ) را برمیگرداند. پرسوجو همچنین شامل یک بررسی بر روی زمان انقضا (Expiry Time) است تا هیچ قطعهای که منقضی شده است را برنگرداند.
همچنین توجه کنید که آیا دوباره از یک پارامتر جایگزین (Placeholder Parameter) برای مقدار id استفاده میکنیم؟
فایل internal/models/snippets.go را باز کنید و کد زیر را اضافه کنید:
package models import ( "database/sql" "errors" // New import "time" ) ... func (m *SnippetModel) Get(id int) (Snippet, error) { // Write the SQL statement we want to execute. Again, I've split it over two // lines for readability. stmt := `SELECT id, title, content, created, expires FROM snippets WHERE expires > UTC_TIMESTAMP() AND id = ?` // Use the QueryRow() method on the connection pool to execute our // SQL statement, passing in the untrusted id variable as the value for the // placeholder parameter. This returns a pointer to a sql.Row object which // holds the result from the database. row := m.DB.QueryRow(stmt, id) // Initialize a new zeroed Snippet struct. var s Snippet // Use row.Scan() to copy the values from each field in sql.Row to the // corresponding field in the Snippet struct. Notice that the arguments // to row.Scan are *pointers* to the place you want to copy the data into, // and the number of arguments must be exactly the same as the number of // columns returned by your statement. err := row.Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires) if err != nil { // If the query returns no rows, then row.Scan() will return a // sql.ErrNoRows error. We use the errors.Is() function check for that // error specifically, and return our own ErrNoRecord error if errors.Is(err, sql.ErrNoRows) { return Snippet{}, ErrNoRecord } else { return Snippet{}, err } } // If everything went OK, then return the filled Snippet struct. return s, nil } ...
در پشت صحنه rows.Scan() درایور شما بهطور خودکار خروجی خام از پایگاه داده SQL را به انواع بومی Go مورد نیاز تبدیل میکند. تا زمانی که با انواعی که بین SQL و Go نگاشت میکنید معقول باشید، این تبدیلها باید بهطور کلی بهخوبی کار کنند. معمولاً:
CHAR،VARCHARوTEXTبهstringنگاشت میشوند.BOOLEANبهboolنگاشت میشود.INTبهintنگاشت میشود؛BIGINTبهint64نگاشت میشود.DECIMALوNUMERICبهfloatنگاشت میشوند.TIME،DATEوTIMESTAMPبهtime.Timeنگاشت میشوند.
اگر در این نقطه سعی کنید برنامه را اجرا کنید، باید یک خطای زمان کامپایل دریافت کنید که میگوید مقدار ErrNoRecord تعریف نشده است:
$ go run ./cmd/web/ # snippetbox.letsgofa.net/internal/models internal/models/snippets.go:82:25: undefined: ErrNoRecord
بیایید اکنون آن را در یک فایل جدید internal/models/errors.go ایجاد کنیم. به این صورت:
$ touch internal/models/errors.go
package models import ( "errors" ) var ErrNoRecord = errors.New("models: no matching record found")
به عنوان یک نکته جانبی، ممکن است بپرسید چرا ما خطای ErrNoRecord را از متد SnippetModel.Get() خود برمیگردانیم، به جای sql.ErrNoRows بهطور مستقیم. دلیل این کار کمک به کپسولهسازی کامل مدل است، بهطوری که هندلرهای ما نگران پایگاه داده زیرین نباشند یا به خطاهای خاص پایگاه داده (مانند sql.ErrNoRows) برای رفتار خود وابسته نباشند.
استفاده از مدل در هندلرهای ما (Using the Model in our Handlers)
بسیار خوب، بیایید متد SnippetModel.Get() را به کار بگیریم.
فایل cmd/web/handlers.go خود را باز کنید و هندلر snippetView را بهروزرسانی کنید تا دادههای یک رکورد خاص را به عنوان یک پاسخ HTTP برگرداند:
package main import ( "errors" // New import "fmt" "html/template" "net/http" "strconv" "snippetbox.letsgofa.net/internal/models" // New import ) ... func (app *application) snippetView(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil || id < 1 { http.NotFound(w, r) return } // Use the SnippetModel's Get() method to retrieve the data for a // specific record based on its ID. If no matching record is found, // return a 404 Not Found response. snippet, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { http.NotFound(w, r) } else { app.serverError(w, r, err) } return } // Write the snippet data as a plain-text HTTP response body. fmt.Fprintf(w, "%+v", snippet) } ...
بیایید این را امتحان کنیم. برنامه را مجدداً راهاندازی کنید، سپس مرورگر وب خود را باز کنید و به http://localhost:4000/snippet/view/1 بروید. باید یک پاسخ HTTP ببینید که شبیه به این است:
همچنین ممکن است بخواهید برخی درخواستها برای قطعات دیگر که منقضی شدهاند یا هنوز وجود ندارند (مانند یک مقدار id برابر با 99) را امتحان کنید تا تأیید کنید که آنها یک پاسخ 404 page not found برمیگردانند:
اطلاعات تکمیلی (Additional Information)
بررسی خطاهای خاص (Checking for Specific Errors)
چندین بار در این فصل از تابع errors.Is() برای بررسی اینکه آیا یک خطا با یک مقدار خاص مطابقت دارد استفاده کردهایم. مانند این:
if errors.Is(err, models.ErrNoRecord) { http.NotFound(w, r) } else { app.serverError(w, r, err) }
در نسخههای بسیار قدیمی Go (قبل از 1.13)، روش ایدئال برای مقایسه خطاها استفاده از عملگر برابری == بود، به این صورت:
if err == models.ErrNoRecord { http.NotFound(w, r) } else { app.serverError(w, r, err) }
اما، در حالی که این کد هنوز کامپایل میشود، استفاده از تابع errors.Is() ایمنتر و بهترین روش است.
این به این دلیل است که Go 1.13 قابلیت افزودن اطلاعات اضافی به خطاها را با بستهبندی آنها معرفی کرد. اگر یک خطا بهطور اتفاقی بستهبندی شود، یک مقدار خطای کاملاً جدید ایجاد میشود — که به نوبه خود به این معنی است که بررسی مقدار خطای اصلی زیرین با استفاده از عملگر برابری معمولی == ممکن نیست.
تابع errors.Is() با باز کردن خطاها در صورت لزوم قبل از بررسی برای مطابقت کار میکند.
همچنین یک تابع دیگر به نام errors.As() وجود دارد که میتوانید از آن برای بررسی اینکه آیا یک خطا (احتمالاً بستهبندی شده) دارای یک نوع خاص است استفاده کنید. ما بعداً در این کتاب از این استفاده خواهیم کرد.
پرس و جوهای تک رکوردی کوتاه (Shorthand Single Record Queries)
من عمداً کد در SnippetModel.Get() را کمی طولانی کردهام تا به وضوح و تأکید بر آنچه در پشت صحنه کد اتفاق میافتد کمک کنم.
در عمل، میتوانید کد را کمی کوتاه کنید با استفاده از این واقعیت که خطاها از DB.QueryRow() تا زمانی که Scan() فراخوانی شود به تعویق میافتند. این هیچ تفاوت عملکردی ندارد، اما اگر بخواهید میتوانید کد را به این صورت بازنویسی کنید:
func (m *SnippetModel) Get(id int) (Snippet, error) { var s Snippet err := m.DB.QueryRow("SELECT ...", id).Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires) if err != nil { if errors.Is(err, sql.ErrNoRows) { return Snippet{}, ErrNoRecord } else { return Snippet{}, err } } return s, nil }
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| رکورد واحد | Single Record | یک ردیف منفرد از دادهها در پایگاه داده |
| کلید اصلی | Primary Key | شناسه منحصر به فرد برای هر رکورد در جدول پایگاه داده |
| پرس و جوهای تک رکوردی | Single Record Queries | کوئریهای SQL که یک رکورد واحد را برمیگردانند |
| زمان انقضا | Expiry Time | زمانی که یک رکورد منقضی میشود و دیگر معتبر نیست |
| پارامتر جایگزین | Placeholder Parameter | نشانگری در کوئری SQL که با مقادیر واقعی جایگزین میشود |
| نگاشت نوع داده | Data Type Mapping | تبدیل خودکار انواع داده بین SQL و زبان برنامهنویسی |
| خطای رکورد یافت نشد | No Record Error | خطایی که زمانی رخ میدهد که رکورد مورد نظر در پایگاه داده پیدا نشود |
| بستهبندی خطا | Error Wrapping | فرآیند اضافه کردن اطلاعات اضافی به یک خطا |