db/query資料夾中創建一個新檔案名為user.sql。從account.sql檔案中複製以下查詢:
-- name: CreateAccount :one
INSERT INTO accounts (
owner,
balance,
currency
) VALUES (
$1, $2, $3
) RETURNING *;
-- name: GetAccount :one
SELECT * FROM accounts
WHERE id = $1 LIMIT 1;
將其貼到新建立的user.sql檔案中。
改變CreateAccount函數名稱為CreateUser。
修改表格名稱為users。
更新欄位名為:username, hashed_password, full_name, 和 email。
不需指定password_changed_at和created_at欄位,因為Postgres會自動以預設值填充這些欄位。
在值列表中添加一個額外的參數,如下:
-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
) RETURNING *;
將GetAccount函數名稱改為GetUser。
查詢更改為從users表中選擇,並根據username獲取用戶資訊,如下:
-- name: GetUser :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
開啟終端機並運行以下命令以生成 Golang 代碼:
❯ make sqlc
sqlc generate
回到Visual Studio Code,你會發現db/sqlc/models.go文件中新增了一個新的User結構。
會看到一個新檔案db/sqlc/user.sql.go,其中包含了兩個函數,用於從資料庫建立和獲取用戶。
user_test.go檔案db/sqlc資料夾中創建一個新檔案名為user_test.go。create和get Account編寫的測試,然後將它們貼到此檔案中。createRandomUser函數將函數名稱更改為createRandomUser。
設置參數類型為CreateUserParams。
函數實作如下:
func createRandomUser(t *testing.T) User {
arg := CreateUserParams{
Username: util.RandomOwner(),
HashedPassword: "secret",
FullName: util.RandomOwner(),
Email: util.RandomEmail(),
}
...
}
RandomEmail函數在util/random.go檔案中實現一個新的RandomEmail函數,如下:
// RandomEmail generates a random email
func RandomEmail() string {
return fmt.Sprintf("%s@email.com", RandomString(6))
}
此函數會返回一個隨機生成的電子郵件地址。
createRandomUser函數回到測試檔案,更改函數調用為testQueries.CreateUser(),並檢查輸出結果是否為一個用戶物件,函數實作如下:
func createRandomUser(t *testing.T) User {
...
user, err := testQueries.CreateUser(context.Background(), arg)
require.NoError(t, err)
require.NotEmpty(t, user)
require.Equal(t, arg.Username, user.Username)
require.Equal(t, arg.HashedPassword, user.HashedPassword)
require.Equal(t, arg.FullName, user.FullName)
require.Equal(t, arg.Email, user.Email)
require.NotZero(t, user.CreatedAt)
require.True(t, user.PasswordChangedAt.IsZero())
return user
}
這裡我們比較輸出用戶的每個欄位與輸入參數,並驗證CreatedAt和PasswordChangedAt欄位的值。
確認 user.PasswordChangedAt 初始為zero timestamp(使用 IsZero() 函數進行檢查)。
TestCreateUser測試函數只需調用createRandomUser函數,如下:
func TestCreateUser(t *testing.T) {
createRandomUser(t)
}
接著為GetUser功能編寫測試,如下:
func TestGetUser(t *testing.T) {
user1 := createRandomUser(t)
user2, err := testQueries.GetUser(context.Background(), user1.Username)
require.NoError(t, err)
require.NotEmpty(t, user2)
require.Equal(t, user1.Username, user2.Username)
require.Equal(t, user1.HashedPassword, user2.HashedPassword)
require.Equal(t, user1.FullName, user2.FullName)
require.Equal(t, user1.Email, user2.Email)
require.WithinDuration(t, user1.PasswordChangedAt, user2.PasswordChangedAt, time.Second)
require.WithinDuration(t, user1.CreatedAt, user2.CreatedAt, time.Second)
}
在這裡,我們確保查詢的輸出(user2)與輸入(user1)匹配。
首先運行TestCreateUser測試。
Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestCreateUser$ github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc 0.378s
接著運行TestGetUser測試。
Running tool: /usr/local/go/bin/go test -timeout 30s -run ^TestGetUser$ github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc
最後使用TablePlus看數據庫中的User Table的Records。

在db/sqlc/account_test.go文件中,當前代碼僅生成一個隨機擁有者,並未與任何現有用戶建立連接。
func createRandomAccount(t *testing.T) Account {
arg := CreateAccountParams{
Owner: util.RandomOwner(),
Balance: util.RandomMoney(),
Currency: util.RandomCurrency(),
}
...
}
為了修復此問題,我們首先需要在數據庫中創建一名用戶。然後,我們會使用新創建的用戶的用戶名作為賬戶擁有者,而不是一個隨機擁有者。
func createRandomAccount(t *testing.T) Account {
user := createRandomUser(t)
arg := CreateAccountParams{
Owner: user.Username,
Balance: util.RandomMoney(),
Currency: util.RandomCurrency(),
}
...
}
run package tests
Running tool: /usr/local/go/bin/go test -timeout 30s -coverprofile=/var/folders/sk/r57q522d46v1h2f7xv98l7mh0000gn/T/vscode-goCJj4SZ/go-code-cover github.com/Kcih4518/simpleBank_2023/db/sqlc
ok github.com/Kcih4518/simpleBank_2023/db/sqlc 0.654s coverage: 80.8% of statements
但是當執行API testing 時會出現以下錯誤:
MockStore未實現db.Store介面。它缺少一些功能的實現。這是因為我們之前運行make sqlc生成代碼時,CreateUser和GetUser兩個新函數已被添加到Querier**介面。而介面是介面的一部分。api/account.go:1: : # github.com/Kcih4518/simpleBank_2023/api [github.com/Kcih4518/simpleBank_2023/api.test]
api/account_test.go:143:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:233:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:343:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:427:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/account_test.go:544:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser)
api/transfer_test.go:209:24: cannot use store (variable of type *mockdb.MockStore) as db.Store value in argument to NewServer: *mockdb.MockStore does not implement db.Store (missing method CreateUser) (typecheck)
Solution:
重新生成MockStore代碼
命令:
shCopy code
❯ make mock
mockgen -package mockdb -destination db/mock/store.go github.com/techschool/simplebank/db/sqlc Store
檢查新生成的代碼
在**db/mock/store.go文件中,您可以看到已添加GetUser和CreateUser**函數的實現。
**CreateUser**函數模擬和預期呼叫的代碼結構如下:
goCopy code
// CreateUser mocks base method
func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (db.User, error) {
...
}
// CreateUser indicates an expected call of CreateUser
func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call {
...
}
**GetUser**函數模擬和預期呼叫的代碼結構如下:
goCopy code
// GetUser mocks base method
func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (db.User, error) {
...
}
// GetUser indicates an expected call of GetUser
func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 interface{}) *gomock.Call {
...
}
重新運行API單元測試
現在API單元測試應該可以運行。讓我們在終端中重新運行**make test**來確認。
make test

伺服器運行後,將在8080端口上監聽和服務HTTP請求。然後,使用Postman測試現有的API以創建一個新的帳戶。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
首次測試將嘗試為不存在於資料庫中的擁有者創建一個帳戶。
遇到的錯誤

在api/account.go文件的創建帳戶處理程序中,如果store.CreateAccount調用返回錯誤,我們將嘗試將其轉換為pq.Error類型,並將結果分配給pqErr變量。如果轉換成功,我們將在此處打印日誌以查看此錯誤的代碼名稱:
func (server *Server) createAccount(ctx *gin.Context) {
...
account, err := server.store.CreateAccount(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
log.Println(pqErr.Code.Name())
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, account)
}
接著,重新啟動服務器,並通過Postman重新發送相同的請求。
foreign_key_violation:當我們嘗試為不存在的用戶創建新帳戶時出現。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
2023/09/19 19:45:08 foreign_key_violation
[GIN] 2023/09/19 - 19:45:08 | 500 | 13.885157ms | ::1 | POST "/accounts"
unique_violation:當我們嘗試為同一擁有者創建超過一個具有相同貨幣的帳戶時出現。
make server
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createAccount-fm (3 handlers)
[GIN-debug] GET /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).getAccount-fm (3 handlers)
[GIN-debug] DELETE /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).delAccount-fm (3 handlers)
[GIN-debug] GET /accounts --> github.com/Kcih4518/simpleBank_2023/api.(*Server).listAccount-fm (3 handlers)
[GIN-debug] PATCH /accounts/:id --> github.com/Kcih4518/simpleBank_2023/api.(*Server).updateAccount-fm (3 handlers)
[GIN-debug] POST /transfers --> github.com/Kcih4518/simpleBank_2023/api.(*Server).createTransfer-fm (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
2023/09/19 19:49:10 unique_violation
[GIN] 2023/09/19 - 19:49:10 | 500 | 18.354167ms | ::1 | POST "/accounts"
現在,我們將更新錯誤處理代碼以處理這些特定錯誤情況。我們將使用一個簡單的switch case語句來檢查錯誤代碼名稱,並根據情況返回相應的HTTP狀態代碼:
func (server *Server) createAccount(ctx *gin.Context) {
...
account, err := server.store.CreateAccount(ctx, arg)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code.Name() {
case "foreign_key_violation", "unique_violation":
ctx.JSON(http.StatusForbidden, errorResponse(err))
return
}
}
ctx.JSON(http.StatusInternalServerError, errorResponse(err))
return
}
ctx.JSON(http.StatusOK, account)
}
403 Forbidden :
{
"error": "pq: insert or update on table \"accounts\" violates foreign key constraint \"accounts_owner_fkey\""
}

403 Forbidden
{
"error": "pq: duplicate key value violates unique constraint \"accounts_owner_currency_idx\""
}
