3 minutes
如何在 Go 裡用 PostgreSQL 實作 CRUD
本篇文章要說的是,資料庫的增刪修,也就是 CRUD(Create、Read、Update、Delete),指的就是建立資料表時所使用的 insert、select、update、delete。
select 的部份還會解釋 scan 的用法,分別是 scan 一個簡單值、一個 struct、struct 切片。
資料庫
Makefile
在此我們先建立一個 Makefile,用以方便產生資料庫與建立資料表。資料表的內容則寫在 init.sql。
DB_NAME := "products_dev"
db.drop:
dropdb --if-exists ${DB_NAME}
db.init: db.create
psql ${DB_NAME} < db/init.sql
db.create: db.drop
createdb ${DB_NAME} || true
建立資料表
以下為腳本 init.sql,用來建立 products 資料表,並為欄位定義。
CREATE TABLE products(
id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS identity,
name TEXT NOT NULL,
price INT NOT NULL,
inserted_at timestamp(0) NOT NULL DEFAULT (now() at time zone'utc'),
updated_at timestamp(0) NOT NULL DEFAULT (now() at time zone 'utc'),
CHECK (price>=0)
);
PostgreSQL
執行返回資料的 SQL 語句時,我們使用了 database/sql
包中提供的 Query
方法。每個都返回一個 row 或 rows,可以使用 Scan 方法將其數據複製到變數。
執行不返回數據的語句時,可使用 Exec
或 ExecContext
方法。常用於:Insert
、Delete
、Update
的 SQL 語句。
database/sql
包,提供兩種執行結果查詢的方法:
- 查詢單行 —
QueryRow
最多從數據庫中返回一行。 - 查詢多行 —
Query
將所有匹配的
新增 Insert
插入數據要注意,MySQL 佔位符用 ?,?,? ,PostgreSQL 用 $1,$2,$3。
SQL 語句為插入兩個欄位的值,並在最後加上 returning *
,返回所插入資料的所有欄位。
const insertProductQuery = `INSERT INTO products("name",price) VALUES ($1,$2) returning *`
// InsertProduct 插入商品
func (ps *ProductService) InsertProduct(params ProductParams) (*Product, error) {
// 插入後返回所有欄位
var i Product
row := ps.db.QueryRow(insertProductQuery, params.Name, params.Price)
err := row.Scan(&i.ID, &i.Name, &i.Price, &i.InsertedAt, &i.UpdatedAt)
return &i, err
}
查詢 Read
首先定義一個 SQL 查詢字串 listProductsQuery
,用來從資料表中選取所有欄位,並依照 id 排序。
定義 ListProducts
方法,它屬於 ProductService
結構,並返回一個包含 Product 指標的切片和一個 error。
使用 ps.db (代表資料庫連線)執行 SQL 查詢,這裡使用 Query
查詢多行,查詢結果儲存在 rows 中,錯誤存在 err 中。
defer rows.Close()
使用 defer
確保當函式結束時,自動關閉 rows
釋放持有的任何資源。
宣告一個 result 的切片,用來儲存查詢結果中的每一筆產品資料,切片中存放的是 product
指標。
rows.Next()
是一個在 Query 裡的一個 Next 方法,可以迭代數據庫的指標,進而獲取每一行的數據,當它迭代到最後一行數據後,會觸發一個 io.EOF
的信號,引發一個錯誤,屆時 go 會自動調用 rows.Close()
方法釋放連接,然後返回 false,此 for 循環就會結束。
宣告一個變數 i ,用來暫存當前行的產品資料,資料型態為 Product
。
rows.Scan()
將當前行的欄位值,依次掃描到 i 欄位中。
將 i 的指標加入到 result
切片中,方便之後返回所有結果。
- 列表查詢
const listProductsQuery = `SELECT * FROM products ORDER BY id`
func (ps *ProductService) ListProducts() ([]*Product, error) {
rows, err := ps.db.Query(listProductsQuery)
if err != nil {
return nil, err
}
defer rows.Close()
var result []*Product
for rows.Next() {
var i Product
err := rows.Scan(&i.ID, &i.Name, &i.Price, &i.InsertedAt, &i.UpdatedAt)
if err != nil {
return nil, err
}
result = append(result, &i)
}
return result, nil
}
- ID 查詢
const getProduyctByID = `SELECT id, name, price, inserted_at, updated_at FROM products where id = $1`
// GetProductByID 查詢商品
func (ps *ProductService) GetProductByID(id int64) (*Product, error) {
row := ps.db.QueryRow(getProductByID, id)
var i Product
if err := row.Scan(&i.ID, &i.Name, &i.Price, &i.InsertedAt, &i.UpdatedAt); err != nil {
return nil, err
}
return &i, nil
}
修改 Update
const updateProduct = `UPDATE products SET name = $1, price = $2, updated_at = (now() at time zone 'utc') where id = $3 returning *`
// UpdateProduct 更改商品內容
func (ps *ProductService) UpdateProduct(id int64, params ProductParams) (*Product, error) {
var i Product
// QueryRow,會傳回最多一行的資料,不會傳零值,error 會延遲到呼叫 row.Scan 為止,若查詢未選到任何行,row.Scan 會傳回 ErrNoRows 。否則,傳回 scan 第一個選定的行並丟棄餘行
row := ps.db.QueryRow(updateProduct, params.Name, params.Price, id)
err := row.Scan(&i.ID, &i.Name, &i.Price, &i.InsertedAt, &i.UpdatedAt)
return &i, err
}
刪除 Delete
const deleteProductQuery = `DELETE FROM products WHERE id = $1`
// DeleteProduct 刪除商品
func (ps *ProductService) DeleteProduct(id int64) error {
_, err := ps.db.Exec(deleteProductQuery, id)
return err
}
主程式
package main
import (
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq"
)
type ProductService struct {
db *sql.DB
}
// Product 更新商品或插入商品所需的參數值
type ProductParams struct {
Name string
Price int32
}
type Product struct {
ID int64
Name string
Price int32
InsertedAt time.Time
UpdatedAt time.Time
}
func main() {
connStr := "postgres://postgres:postgres@localhost/products_dev?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal(err)
}
// 插入商品 Create
srv := ProductService{db}
product, err := srv.InsertProduct(ProductParams{
Name: "Oolong",
Price: 90,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", product)
// 查詢資料 Read
products, err := srv.ListProducts()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", products)
// 查詢資料 Read by ID
product, err = srv.GetProductByID(product.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", product)
// 更新商品 Update
product, err = srv.UpdateProduct(product.ID, ProductParams{
Name: "milk",
Price: 200,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("%#v\n", product)
// 刪除商品 Delete
err = srv.DeleteProduct(2)
log.Println(err)
}
621 Words
2024-11-06 00:00