Khi thực thi một câu lệnh SQL có trả về dữ liệu, hãy dùng một trong các hàm Query mà package database/sql cung cấp. Mỗi hàm trong số đó trả về một Row hoặc Rows mà bạn có thể sao chép dữ liệu sang các biến bằng hàm Scan. Chẳng hạn, bạn sẽ dùng những hàm này để thực thi câu lệnh SELECT.
Khi thực thi một câu lệnh không trả về dữ liệu, bạn có thể dùng hàm Exec hoặc ExecContext thay thế. Tìm hiểu thêm tại Thực thi câu lệnh SQL không trả về dữ liệu.
Package database/sql cung cấp hai cách để thực thi một truy vấn lấy kết quả.
- Truy vấn một dòng duy nhất –
QueryRowtrả về nhiều nhất mộtRowtừ cơ sở dữ liệu. Tìm hiểu thêm tại Truy vấn một dòng duy nhất. - Truy vấn nhiều dòng –
Querytrả về mọi dòng khớp dưới dạng một structRowsmà mã của bạn có thể lặp qua. Tìm hiểu thêm tại Truy vấn nhiều dòng.
Nếu mã của bạn sẽ thực thi lặp đi lặp lại cùng một câu lệnh SQL, hãy cân nhắc dùng prepared statement. Tìm hiểu thêm tại Dùng prepared statement.
Chú ý: Đừng dùng các hàm định dạng chuỗi như fmt.Sprintf để ghép một câu lệnh SQL! Bạn có thể tạo ra nguy cơ SQL injection. Tìm hiểu thêm tại Tránh nguy cơ SQL injection.
Truy vấn một dòng duy nhất
QueryRow lấy về nhiều nhất một dòng của cơ sở dữ liệu, chẳng hạn khi bạn muốn tra cứu dữ liệu theo một ID duy nhất. Nếu truy vấn trả về nhiều dòng, hàm Scan sẽ bỏ qua tất cả trừ dòng đầu tiên.
QueryRowContext hoạt động giống QueryRow nhưng có thêm đối số context.Context. Tìm hiểu thêm tại Huỷ các thao tác đang chạy.
Ví dụ sau dùng một truy vấn để tìm xem tồn kho có đủ để phục vụ một lần mua hàng hay không. Câu lệnh SQL trả về true nếu đủ, false nếu không. Row.Scan sao chép giá trị trả về kiểu boolean vào biến enough thông qua một con trỏ.
func canPurchase(id int, quantity int) (bool, error) {
var enough bool
// Query for a value based on a single row.
if err := db.QueryRow("SELECT (quantity >= ?) from album where id = ?",
quantity, id).Scan(&enough); err != nil {
if err == sql.ErrNoRows {
return false, fmt.Errorf("canPurchase %d: unknown album", id)
}
return false, fmt.Errorf("canPurchase %d: %v", id, err)
}
return enough, nil
}
Lưu ý: Placeholder cho tham số trong prepared statement khác nhau tuỳ theo DBMS và driver mà bạn đang dùng. Ví dụ, driver pq dành cho Postgres yêu cầu placeholder dạng $1 thay vì ?.
Xử lý lỗi
Bản thân QueryRow không trả về lỗi. Thay vào đó, Scan báo mọi lỗi phát sinh từ thao tác tra cứu và scan gộp lại. Nó trả về sql.ErrNoRows khi truy vấn không tìm thấy dòng nào.
Các hàm trả về một dòng duy nhất
| Hàm | Mô tả |
|---|---|
DB.QueryRowDB.QueryRowContext |
Chạy riêng lẻ một truy vấn trả về một dòng. |
Tx.QueryRowTx.QueryRowContext |
Chạy một truy vấn trả về một dòng bên trong một transaction lớn hơn. Tìm hiểu thêm tại Thực thi transaction. |
Stmt.QueryRowStmt.QueryRowContext |
Chạy một truy vấn trả về một dòng bằng một câu lệnh đã được chuẩn bị sẵn. Tìm hiểu thêm tại Dùng prepared statement. |
Conn.QueryRowContext |
Dùng với các kết nối được giữ riêng. Tìm hiểu thêm tại Quản lý kết nối. |
Truy vấn nhiều dòng
Bạn có thể truy vấn nhiều dòng bằng Query hoặc QueryContext, chúng trả về một Rows đại diện cho kết quả truy vấn. Mã của bạn duyệt qua các dòng trả về bằng Rows.Next. Mỗi lần lặp gọi Scan để sao chép giá trị các cột vào biến.
QueryContext hoạt động giống Query nhưng có thêm đối số context.Context. Tìm hiểu thêm tại Huỷ các thao tác đang chạy.
Ví dụ sau thực thi một truy vấn để trả về các album của một nghệ sĩ được chỉ định. Các album được trả về trong một sql.Rows. Mã dùng Rows.Scan để sao chép giá trị các cột vào những biến được biểu diễn bằng con trỏ.
func albumsByArtist(artist string) ([]Album, error) {
rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
if err != nil {
return nil, err
}
defer rows.Close()
// An album slice to hold data from returned rows.
var albums []Album
// Loop through rows, using Scan to assign column data to struct fields.
for rows.Next() {
var alb Album
if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist,
&alb.Price, &alb.Quantity); err != nil {
return albums, err
}
albums = append(albums, alb)
}
if err = rows.Err(); err != nil {
return albums, err
}
return albums, nil
}
Hãy để ý lời gọi rows.Close được defer. Nó giải phóng mọi tài nguyên mà rows đang giữ, bất kể hàm kết thúc theo cách nào. Lặp hết toàn bộ các dòng cũng đóng rows một cách ngầm định, nhưng tốt hơn là dùng defer để chắc chắn rằng rows luôn được đóng.
Lưu ý: Placeholder cho tham số trong prepared statement khác nhau tuỳ theo DBMS và driver mà bạn đang dùng. Ví dụ, driver pq dành cho Postgres yêu cầu placeholder dạng $1 thay vì ?.
Xử lý lỗi
Hãy nhớ kiểm tra lỗi từ sql.Rows sau khi lặp qua kết quả truy vấn. Nếu truy vấn thất bại, đây là cách mã của bạn biết được điều đó.
Các hàm trả về nhiều dòng
| Hàm | Mô tả |
|---|---|
DB.QueryDB.QueryContext |
Chạy riêng lẻ một truy vấn. |
Tx.QueryTx.QueryContext |
Chạy một truy vấn bên trong một transaction lớn hơn. Tìm hiểu thêm tại Thực thi transaction. |
Stmt.QueryStmt.QueryContext |
Chạy một truy vấn bằng một câu lệnh đã được chuẩn bị sẵn. Tìm hiểu thêm tại Dùng prepared statement. |
Conn.QueryContext |
Dùng với các kết nối được giữ riêng. Tìm hiểu thêm tại Quản lý kết nối. |
Xử lý giá trị cột có thể null
Package database/sql cung cấp một số kiểu dữ liệu đặc biệt mà bạn có thể dùng làm đối số cho hàm Scan khi giá trị của một cột có thể là null. Mỗi kiểu đều có một trường Valid cho biết giá trị có khác null hay không, và một trường chứa giá trị đó nếu có.
Mã trong ví dụ sau truy vấn tên của một khách hàng. Nếu giá trị tên là null, mã sẽ thay bằng một giá trị khác để dùng trong ứng dụng.
var s sql.NullString
err := db.QueryRow("SELECT name FROM customer WHERE id = ?", id).Scan(&s)
if err != nil {
log.Fatal(err)
}
// Find customer name, using placeholder if not present.
name := "Valued Customer"
if s.Valid {
name = s.String
}
Xem thêm về từng kiểu trong tài liệu tham chiếu của package sql:
Lấy dữ liệu từ các cột
Khi lặp qua các dòng mà một truy vấn trả về, bạn dùng Scan để sao chép giá trị các cột của một dòng vào các giá trị Go, như mô tả trong tài liệu tham chiếu Rows.Scan.
Có một tập chuyển đổi dữ liệu cơ bản được mọi driver hỗ trợ, chẳng hạn chuyển INT của SQL sang int của Go. Một số driver mở rộng tập chuyển đổi này; xem tài liệu của từng driver để biết chi tiết.
Như bạn có thể đoán, Scan sẽ chuyển từ kiểu của cột sang kiểu dữ liệu Go tương tự. Ví dụ, Scan sẽ chuyển từ CHAR, VARCHAR và TEXT của SQL sang string của Go. Tuy nhiên, Scan cũng sẽ thực hiện chuyển đổi sang một kiểu Go khác phù hợp với giá trị của cột. Ví dụ, nếu cột là VARCHAR nhưng luôn chứa một con số, bạn có thể chỉ định một kiểu số của Go, chẳng hạn int, để nhận giá trị, và Scan sẽ chuyển đổi giúp bạn bằng strconv.Atoi.
Để biết chi tiết hơn về các chuyển đổi mà hàm Scan thực hiện, xem tài liệu tham chiếu Rows.Scan.
Xử lý nhiều tập kết quả
Khi thao tác cơ sở dữ liệu của bạn có thể trả về nhiều tập kết quả, bạn có thể lấy chúng bằng Rows.NextResultSet. Điều này hữu ích chẳng hạn khi bạn gửi câu lệnh SQL truy vấn riêng nhiều bảng, mỗi bảng trả về một tập kết quả.
Rows.NextResultSet chuẩn bị tập kết quả tiếp theo để một lời gọi Rows.Next lấy được dòng đầu tiên của tập kế tiếp đó. Nó trả về một giá trị boolean cho biết liệu có tập kết quả tiếp theo hay không.
Mã trong ví dụ sau dùng DB.Query để thực thi hai câu lệnh SQL. Tập kết quả đầu tiên đến từ truy vấn thứ nhất trong thủ tục, lấy về toàn bộ các dòng trong bảng album. Tập kết quả kế tiếp đến từ truy vấn thứ hai, lấy về các dòng từ bảng song.
rows, err := db.Query("SELECT * from album; SELECT * from song;")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Loop through the first result set.
for rows.Next() {
// Handle result set.
}
// Advance to next result set.
rows.NextResultSet()
// Loop through the second result set.
for rows.Next() {
// Handle second set.
}
// Check for any error in either result set.
if err := rows.Err(); err != nil {
log.Fatal(err)
}