目前,我們已為簡易銀行系統實現了多項功能,包括創建、更新、檢索或轉賬等,然而,還缺少一個非常重要的功能:user authentication
和 authorization
,在這項功能被實現之前,我們的銀行系統無法完全完成。
因此,今天我們將採取實現它的第一步,即向資料庫中新增一個User Table,並通過一些資料庫constraints
將其與現有的帳戶表Link。
Table accounts as A {
id bigserial [pk]
owner varchar [ not null]
balance bigint [not null]
currency varchar [not null]
created_at timestamptz [not null,default: `now()`]
Indexes {
owner
(owner, currency) [unique]
}
}
Table entries {
id bigserial [pk]
account_id bigint [ref: > A.id,not null]
amount bigint [not null,note: 'can be negative or positive']
created_at timestamptz [not null,default: `now()`]
Indexes {
account_id
}
}
Table transfers {
id bigserial [pk]
from_account_id bigint [ref: > A.id,not null]
to_account_id bigint [ref: > A.id,not null]
amount bigint [not null,note: 'must be positive']
created_at timestamptz [not null,default: `now()`]
Indexes {
from_account_id
to_account_id
(from_account_id, to_account_id)
}
}
Account
Table有一個「owner」欄位,可以用來識別該帳戶的所有者。Table users as U {
username varchar [pk]
hashed_password varchar [not null]
full_name varchar [not null]
email varchar [unique,not null]
password_changed_at timestamptz [not null,default: '0001-01-01 00:00:00Z']
created_at timestamptz [not null,default: `now()`]
}
User
Table。Table users as U {
username varchar [pk]
hashed_password varchar [not null]
full_name varchar [not null]
email varchar [unique,not null]
password_changed_at timestamptz [not null,default: '0001-01-01 00:00:00Z']
created_at timestamptz [not null,default: `now()`]
}
Table accounts as A {
id bigserial [pk]
owner varchar [ref: > U.username, not null]
balance bigint [not null]
currency varchar [not null]
created_at timestamptz [not null,default: `now()`]
Indexes {
owner
}
}
user
擁有多個具有不同貨幣的accounts
。account
中的「owner
」欄位連結到用戶表格中的「username
」欄位來實現這一點,使得「owner」欄位成為外部鍵。user
" 可以擁有多個 "accounts
",但一個 "account
" 只能屬於一個 "user
"。Table accounts as A {
id bigserial [pk]
owner varchar [ref: > U.username, not null]
balance bigint [not null]
currency varchar [not null]
created_at timestamptz [not null, default: `now()`]
Indexes {
owner
(owner, currency) [unique]
}
}
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "accounts" (
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "entries" (
"id" bigserial PRIMARY KEY,
"account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "transfers" (
"id" bigserial PRIMARY KEY,
"from_account_id" bigint NOT NULL,
"to_account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE INDEX ON "accounts" ("owner");
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
CREATE INDEX ON "entries" ("account_id");
CREATE INDEX ON "transfers" ("from_account_id");
CREATE INDEX ON "transfers" ("to_account_id");
CREATE INDEX ON "transfers" ("from_account_id", "to_account_id");
COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive';
COMMENT ON COLUMN "transfers"."amount" IS 'must be positive';
ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id");
Then the alter table
command to add foreign key constraint to the owner
field:
ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
And a composite unique index for the owner
and currency
.
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
需要將新的變更添加到simple bank
中。
一種方法是替換 init_schema 遷移文件的全部內容,然後重置數據庫並重新運行 migrate up 命令,但這在Production環境中並不是正確的方法。
為什麽直接替換並重新migrattion不是正確的方法?
正確的方法是創建一個新的遷移版本。我們可以打開終端機來生成一個新的遷移。
要創建一個新的遷移,我們運行以下命令:
migrate create -ext sql -dir db/migration -seq add_users
使用一些參數來告訴 migrate 設置輸出文件的擴展名為 sql,輸出目錄為 db/migration,使用順序號作為文件名前綴,並將遷移名稱設置為 add_users。
migrate create -ext sql -dir db/migration -seq add_users
可以看到,在 db/migration 文件夾中生成了兩個遷移文件。
❯ git status
On branch feat-building-restful-http-json-api-gin
Untracked files:
(use "git add <file>..." to include in what will be committed)
db/migration/000002_add_users.down.sql
db/migration/000002_add_users.up.sql
nothing added to commit but untracked files present (use "git add" to track)
創建 users 表格,可以復制 dbdiagram 為我們生成的 SQL 查詢並將其粘貼到遷移文件 000002_add_users.up.sql 中。
接著,將複製 accounts 的 owner 字段的新外鍵約束的查詢。
最後,還要複製 owner 和 currency 的UNIQUE
復合索引的查詢,具體 SQL 語句如下:
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"password_changed_at" timestamptz NOT NULL DEFAULT('0001-01-01 00:00:00Z'),
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
上述的SQL已經足夠,但有另一種確保每個 owner 對特定 currency 最多只有一個賬戶的方法,可以添加一個對 accounts 表的 owner 和 currency pair 的唯一約束,而不是直接使用這樣的唯一索引,該命令與上面添加外鍵約束的命令非常相似,SQL 語句如下:
-- CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency");
實際上,添加這樣的唯一約束會自動創建與我們上面寫的命令相同的用於 owner 和 currency 的唯一復合索引。
UNIQUE INDEX
vs CONSTRAINT
?
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency");
make migrateup
migrate -path db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose up
2023/09/19 12:13:10 Start buffering 2/u add_users
2023/09/19 12:13:10 Read and execute 2/u add_users
2023/09/19 12:13:10 error: migration failed: insert or update on table "accounts" violates foreign key constraint "accounts_owner_fkey", Key (owner)=(pvtpxw) is not present in table "users". in line 0: CREATE TABLE "users"
(
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"password_changed_at" timestamptz NOT NULL DEFAULT('0001-01-01 00:00:00Z'),
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
ALTER TABLE "accounts" ADD CONSTRAINT "owner_currency_key" UNIQUE ("owner", "currency");
(details: pq: insert or update on table "accounts" violates foreign key constraint "accounts_owner_fkey")
make: *** [migrateup] Error 1
運行 make migrateup
指令來應用新的遷移。
accounts
表中已有的行的 owner
字段包含隨機值,並未鏈接到任何存在的用戶。需要清除所有現有數據再運行 migrate up 指令。
由於之前的 migrate up 執行失敗,當前的模式遷移已變為“dirty state”版本 2。
嘗試運行 make migratedown
來清理數據會導致錯誤,因為目前處於 dirty version。
make migratedown
指令。運行 make migratedown
指令成功,數據庫中的所有表都被刪除。
make migrateup
指令來恢復它們。運行 make migrateup
指令成功,可以在 TablePlus 中看到新的 users
表。
users
表的結構包括我們在代碼中聲明的所有字段:username
, hashed_password
, full_name
, email
。
password_changed_at
和 created_at
字段都有正確的默認值。
底部有兩個 BTREE 索引:一個用於主鍵 username
(必須是唯一的),另一個用於 email
(也是唯一索引)。
檢查 accounts
表:
owner
字段現在包含一個外鍵約束,鏈接到 users
表的 username
字段。owner
和 currency
對的唯一索引。Makefile
新增migrateup1
( -verbose up 1
)
migrateup1:
migrate -path db/migration -database "$(DB_URL)" -verbose up 1
.PHONY: network postgres createdb dropdb migrateup migratedown sqlc test clean server air mock migrateup1
我們需要migration down 與migration up來完成migration的步驟。
首先,我們需要刪除“accounts”表中“owner”和“currency”配對的唯一約束,使用的命令如下:
ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "owner_currency_key";
接著,我們需要刪除“owner”字段的外鍵約束,但首先我們需要知道這個外鍵約束的名字。為了找到它,我們可以返回Table Plus並點擊窗口底部的Info按鈕來查看表定義。
在表定義中,我們可以找到外鍵約束的名稱:accounts_owner_fkey
。我們需要將其複製並粘貼到以下命令中:
ALTER TABLE IF EXISTS "accounts" DROP CONSTRAINT IF EXISTS "accounts_owner_fkey";
最後一步是刪除“users”表,命令如下:
DROP TABLE IF EXISTS "users";
Makefile
新增migratedown1
( -verbose down 1
)
migratedown1:
migrate -path db/migration -database "$(DB_URL)" -verbose down 1
.PHONY: network postgres createdb dropdb migrateup migratedown sqlc test clean server air mock migrateup1 migratedown1
make migratedown1
make migrateup1
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "accounts" (
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "entries" (
"id" bigserial PRIMARY KEY,
"account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "transfers" (
"id" bigserial PRIMARY KEY,
"from_account_id" bigint NOT NULL,
"to_account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE INDEX ON "accounts" ("owner");
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
CREATE INDEX ON "entries" ("account_id");
CREATE INDEX ON "transfers" ("from_account_id");
CREATE INDEX ON "transfers" ("to_account_id");
CREATE INDEX ON "transfers" ("from_account_id", "to_account_id");
COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive';
COMMENT ON COLUMN "transfers"."amount" IS 'must be positive';
ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "users" ("username");
ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id");