اجرای دستورات SQL (Executing SQL Statements)
حالا بیایید متد SnippetModel.Insert() را که تازه ایجاد کردهایم بهروزرسانی کنیم تا یک رکورد جدید (New Record) در جدول snippets ما ایجاد کند و سپس شناسه عددی id برای رکورد جدید را برگرداند.
برای انجام این کار، میخواهیم کوئری SQL (SQL Query) زیر را در پایگاه داده خود اجرا کنیم:
INSERT INTO snippets (title, content, created, expires) VALUES(?, ?, UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? DAY))
توجه کنید که در این کوئری از کاراکتر ? برای نشان دادن پارامترهای جایگزین (Placeholder Parameters) برای دادههایی که میخواهیم در پایگاه داده وارد کنیم استفاده میکنیم؟ از آنجایی که دادههایی که استفاده خواهیم کرد در نهایت ورودی غیرقابل اعتماد کاربر (Untrusted User Input) از یک فرم خواهد بود، استفاده از پارامترهای جایگزین به جای درج مستقیم داده در کوئری SQL یک روش خوب است.
اجرای کوئری (Executing the Query)
Go سه روش مختلف برای اجرای کوئریهای پایگاه داده ارائه میدهد:
DB.Query()برای کوئریهایSELECTکه چندین سطر برمیگردانند استفاده میشود.DB.QueryRow()برای کوئریهایSELECTکه یک سطر برمیگردانند استفاده میشود.DB.Exec()برای دستوراتی که سطری برنمیگردانند (مانندINSERTوDELETE) استفاده میشود.
بنابراین، در مورد ما، مناسبترین ابزار برای این کار DB.Exec() است. بیایید مستقیماً وارد عمل شویم و نحوه استفاده از آن را در متد SnippetModel.Insert() نشان دهیم. جزئیات را بعداً بررسی خواهیم کرد.
فایل internal/models/snippets.go خود را باز کنید و آن را به این صورت بهروزرسانی کنید:
package models ... type SnippetModel struct { DB *sql.DB } func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) { // Write the SQL statement we want to execute. I've split it over two lines // for readability (which is why it's surrounded with backquotes instead // of normal double quotes). stmt := `INSERT INTO snippets (title, content, created, expires) VALUES(?, ?, UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? DAY))` // Use the Exec() method on the embedded connection pool to execute the // statement. The first parameter is the SQL statement, followed by the // values for the placeholder parameters: title, content and expiry in // that order. This method returns a sql.Result type, which contains some // basic information about what happened when the statement was executed. result, err := m.DB.Exec(stmt, title, content, expires) if err != nil { return 0, err } // Use the LastInsertId() method on the result to get the ID of our // newly inserted record in the snippets table. id, err := result.LastInsertId() if err != nil { return 0, err } // The ID returned has the type int64, so we convert it to an int type // before returning. return int(id), nil } ...
بیایید سریعاً درباره نوع sql.Result که توسط DB.Exec() برگردانده میشود صحبت کنیم. این دو متد را ارائه میدهد:
LastInsertId()- که عدد صحیح (یکint64) تولید شده توسط پایگاه داده در پاسخ به یک دستور را برمیگرداند. معمولاً این از یک ستون "افزایش خودکار" هنگام درج یک سطر جدید است، که دقیقاً همان چیزی است که در مورد ما اتفاق میافتد.RowsAffected()- که تعداد سطرهای (به صورتint64) تحت تأثیر دستور را برمیگرداند.
_, err := m.DB.Exec("INSERT INTO ...", ...)
استفاده از مدل در هندلرهای ما (Using the Model in our Handlers)
بیایید این را به چیزی ملموستر برگردانیم و نشان دهیم چگونه این کد جدید را از هندلرهای خود فراخوانی کنیم. فایل cmd/web/handlers.go خود را باز کنید و هندلر snippetCreatePost را به این صورت بهروزرسانی کنید:
package main ... func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) { // Create some variables holding dummy data. We'll remove these later on // during the build. title := "O snail" content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa" expires := 7 // Pass the data to the SnippetModel.Insert() method, receiving the // ID of the new record back. id, err := app.snippets.Insert(title, content, expires) if err != nil { app.serverError(w, r, err) return } // Redirect the user to the relevant page for the snippet. http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther) }
برنامه را اجرا کنید، سپس یک پنجره ترمینال دوم باز کنید و از curl برای ارسال یک درخواست POST /snippet/create استفاده کنید، به این صورت (توجه کنید که پرچم -L به curl دستور میدهد که به طور خودکار ریدایرکتها را دنبال کند):
$ curl -iL -d "" http://localhost:4000/snippet/create HTTP/1.1 303 See Other Location: /snippet/view/4 Date: Wed, 18 Mar 2024 11:29:23 GMT Content-Length: 0 HTTP/1.1 200 OK Date: Wed, 18 Mar 2024 11:29:23 GMT Content-Length: 39 Content-Type: text/plain; charset=utf-8 Display a specific snippet with ID 4...
پس این به خوبی کار میکند. ما یک درخواست HTTP ارسال کردیم که هندلر snippetCreatePost ما را فعال کرد، که به نوبه خود متد SnippetModel.Insert() ما را فراخوانی کرد. این یک رکورد جدید در پایگاه داده درج کرد و شناسه این رکورد جدید را برگرداند. سپس هندلر ما یک ریدایرکت به URL دیگری با شناسه درج شده صادر کرد.
راحت باشید و نگاهی به جدول snippets در پایگاه داده MySQL خود بیندازید. باید رکورد جدید با شناسه 4 مشابه این را ببینید:
mysql> SELECT id, title, expires FROM snippets; +----+------------------------+---------------------+ | id | title | expires | +----+------------------------+---------------------+ | 1 | An old silent pond | 2025-03-18 10:00:26 | | 2 | Over the wintry forest | 2025-03-18 10:00:26 | | 3 | First autumn morning | 2024-03-25 10:00:26 | | 4 | O snail | 2024-03-25 10:13:04 | +----+------------------------+---------------------+ 4 rows in set (0.00 sec)
اطلاعات تکمیلی (Additional Information)
پارامترهای جایگزین (Placeholder Parameters)
در کد بالا، ما عبارت SQL خود را با استفاده از پارامترهای جایگزین ساختیم، جایی که ? به عنوان جایگزینی برای دادههایی که میخواهیم درج کنیم عمل میکرد.
دلیل استفاده از پارامترهای جایگزین برای ساخت کوئری ما (به جای درج مستقیم رشته) کمک به جلوگیری از حملات تزریق SQL (SQL Injection Attacks) از هر ورودی غیرقابل اعتماد کاربر است.
در پشت صحنه، متد DB.Exec() در سه مرحله کار میکند:
یک دستور آماده (Prepared Statement) جدید در پایگاه داده با استفاده از عبارت SQL ارائه شده ایجاد میکند. پایگاه داده عبارت را تجزیه و کامپایل میکند، سپس آن را برای اجرا آماده نگه میدارد.
در یک مرحله جداگانه دوم،
DB.Exec()مقادیر پارامتر (Parameter Values) را به پایگاه داده منتقل میکند. پایگاه داده سپس دستور آماده را با استفاده از این پارامترها اجرا میکند.سپس دستور آماده را در پایگاه داده میبندد (یا آزاد میکند (Deallocates)).
نحو پارامتر جایگزین بسته به پایگاه داده شما متفاوت است. MySQL، SQL Server و SQLite از نشانهگذاری ? استفاده میکنند، اما PostgreSQL از نشانهگذاری $N استفاده میکند. به عنوان مثال، اگر به جای آن از PostgreSQL استفاده میکردید، مینوشتید:
_, err := m.DB.Exec("INSERT INTO ... VALUES ($1, $2, $3)", ...)
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| دستورات SQL | SQL Statements | دستورالعملهای استاندارد برای تعامل با پایگاه داده |
| رکورد جدید | New Record | یک ردیف جدید از دادهها در پایگاه داده |
| کوئری SQL | SQL Query | دستوری برای بازیابی یا تغییر دادهها در پایگاه داده |
| پارامترهای جایگزین | Placeholder Parameters | نشانههایی در کوئری SQL که بعداً با مقادیر واقعی جایگزین میشوند |
| ورودی غیرقابل اعتماد کاربر | Untrusted User Input | دادههای ورودی از کاربر که باید قبل از استفاده اعتبارسنجی شوند |
| حملات تزریق SQL | SQL Injection Attacks | تلاش برای وارد کردن کد مخرب از طریق ورودیهای کاربر به پایگاه داده |
| دستور آماده | Prepared Statement | یک کوئری SQL از پیش کامپایل شده که با پارامترها اجرا میشود |
| مقادیر پارامتر | Parameter Values | دادههای واقعی که جایگزین پارامترهای جایگزین میشوند |
| آزادسازی | Deallocation | فرآیند آزاد کردن منابع سیستم که توسط یک دستور آماده استفاده شدهاند |