Let's Go پاسخ‌های مبتنی بر پایگاه داده › اجرای دستورات SQL (Executing SQL Statements)
قبلی · فهرست · بعدی
فصل ۴.۶

اجرای دستورات 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.Exec() است. بیایید مستقیماً وارد عمل شویم و نحوه استفاده از آن را در متد SnippetModel.Insert() نشان دهیم. جزئیات را بعداً بررسی خواهیم کرد.

فایل internal/models/snippets.go خود را باز کنید و آن را به این صورت به‌روزرسانی کنید:

File: 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() برگردانده می‌شود صحبت کنیم. این دو متد را ارائه می‌دهد:

_, err := m.DB.Exec("INSERT INTO ...", ...)

استفاده از مدل در هندلرهای ما (Using the Model in our Handlers)

بیایید این را به چیزی ملموس‌تر برگردانیم و نشان دهیم چگونه این کد جدید را از هندلرهای خود فراخوانی کنیم. فایل cmd/web/handlers.go خود را باز کنید و هندلر snippetCreatePost را به این صورت به‌روزرسانی کنید:

File: cmd/web/handlers.go
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() در سه مرحله کار می‌کند:

  1. یک دستور آماده (Prepared Statement) جدید در پایگاه داده با استفاده از عبارت SQL ارائه شده ایجاد می‌کند. پایگاه داده عبارت را تجزیه و کامپایل می‌کند، سپس آن را برای اجرا آماده نگه می‌دارد.

  2. در یک مرحله جداگانه دوم، DB.Exec() مقادیر پارامتر (Parameter Values) را به پایگاه داده منتقل می‌کند. پایگاه داده سپس دستور آماده را با استفاده از این پارامترها اجرا می‌کند.

  3. سپس دستور آماده را در پایگاه داده می‌بندد (یا آزاد می‌کند (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 فرآیند آزاد کردن منابع سیستم که توسط یک دستور آماده استفاده شده‌اند