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

اجرای دستورات SQL

حالا بیایید متد SnippetModel.Insert() را به‌روزرسانی کنیم — که تازه ساختیم — تا یک رکورد جدید در جدول snippets ما ایجاد کند و سپس عدد صحیح id برای رکورد جدید را برگرداند.

برای انجام این کار، می‌خواهیم پرس‌وجوی SQL زیر را روی پایگاه داده خود اجرا کنیم:

INSERT INTO snippets (title, content, created, expires)
VALUES(?, ?, UTC_TIMESTAMP(), DATE_ADD(UTC_TIMESTAMP(), INTERVAL ? DAY))

توجه کنید که در این پرس‌وجو چگونه از کاراکتر ? برای نشان دادن پارامترهای placeholder برای داده‌هایی که می‌خواهیم در پایگاه داده درج کنیم استفاده می‌کنیم؟ چون داده‌هایی که استفاده می‌کنیم در نهایت ورودی کاربر غیرقابل اعتماد از یک فرم خواهد بود، استفاده از پارامترهای placeholder به جای درون‌یابی داده در پرس‌وجوی SQL یک روش خوب است.

اجرای پرس‌وجو

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

همچنین، کاملاً قابل قبول (و رایج) است که مقدار برگشتی sql.Result را نادیده بگیرید اگر به آن نیاز ندارید. مانند این:

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

استفاده از مدل در handlerهای ما

بیایید این را به چیزی ملموس‌تر برگردانیم و نحوه فراخوانی این کد جدید را از handlerهای خود نشان دهیم. فایل cmd/web/handlers.go خود را باز کنید و handler 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 دستور می‌دهد که به طور خودکار redirectها را دنبال کند):

$ 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 ارسال کردیم که handler snippetCreatePost ما را فعال کرد، که به نوبه خود متد SnippetModel.Insert() ما را فراخوانی کرد. این یک رکورد جدید در پایگاه داده درج کرد و ID این رکورد جدید را برگرداند. سپس handler ما یک redirect به URL دیگری با ID درون‌یابی شده صادر کرد.

می‌توانید در جدول snippets پایگاه داده MySQL خود نگاهی بیندازید. باید رکورد جدید با ID 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)

اطلاعات اضافی

پارامترهای placeholder

در کد بالا، دستور SQL خود را با استفاده از پارامترهای placeholder ساختیم، جایی که ? به عنوان یک placeholder برای داده‌هایی که می‌خواهیم درج کنیم عمل می‌کرد.

دلیل استفاده از پارامترهای placeholder برای ساخت پرس‌وجوی ما (به جای درون‌یابی رشته) کمک به جلوگیری از حملات تزریق SQL (SQL injection) از هر ورودی ارائه شده توسط کاربر غیرقابل اعتماد است.

در پشت صحنه، متد DB.Exec() در سه مرحله کار می‌کند:

  1. یک دستور آماده جدید روی پایگاه داده با استفاده از دستور SQL ارائه شده ایجاد می‌کند. پایگاه داده دستور را تجزیه و کامپایل می‌کند، سپس آن را برای اجرا ذخیره می‌کند.

  2. در یک مرحله جداگانه دوم، DB.Exec() مقادیر پارامتر را به پایگاه داده ارسال می‌کند. سپس پایگاه داده دستور آماده را با استفاده از این پارامترها اجرا می‌کند. چون پارامترها بعداً، پس از کامپایل شدن دستور، منتقل می‌شوند، پایگاه داده آن‌ها را به عنوان داده خالص در نظر می‌گیرد. آن‌ها نمی‌توانند قصد دستور را تغییر دهند. تا زمانی که دستور اصلی از داده‌های غیرقابل اعتماد مشتق نشده باشد، تزریق نمی‌تواند رخ دهد.

  3. سپس دستور آماده را روی پایگاه داده می‌بندد (یا تخصیص‌زدایی می‌کند).

نحو پارامتر placeholder بسته به پایگاه داده شما متفاوت است. MySQL، SQL Server و SQLite از نماد ? استفاده می‌کنند، اما PostgreSQL از نماد $N استفاده می‌کند. به عنوان مثال، اگر به جای آن از PostgreSQL استفاده می‌کردید، می‌نوشتید:

_, err := m.DB.Exec("INSERT INTO ... VALUES ($1, $2, $3)", ...)