本篇文章要說的是,資料庫的增刪修,也就是 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 方法將其數據複製到變數。

執行不返回數據的語句時,可使用 ExecExecContext 方法。常用於:InsertDeleteUpdate 的 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)
}