跳至主要内容

關聯式資料庫 RDBMS

在遊戲營運的過程中,一定會產生某些資料是必須被保存下來的,理由是遊戲的過程通常會發生交易,為了能夠驗證交易行為的正當性會須要依賴遊戲結果;或者是如棋牌類遊戲中,通常會實現牌譜功能供玩家進行復盤,這個時候就必須連遊戲過程也保存下來。由於上述的資料是會被不同的玩家與營運方即時查詢,查詢的依據可能是某一位玩家ID、某一段時間內、或是某一局遊戲的資料。基於上述特性,我們可以知道這些數據很適合使用關聯式資料庫做儲存。

為了使用關聯式資料庫,我們可能會需要撰寫結構化查詢語言 Structured Query Language,將讀取出來的資料轉換成結構物件,或是利用物件關聯對映​ Object–relational mapping工具,達成上述需求。但不論是選擇哪一個方法,我們都需要經歷過連線的初始化,查詢語句的生成,這些流程應該被適度封裝在此目錄下,讓遊戲與管理系統的邏輯能夠方便的調用。

作者認為資料庫以及儲存體Repository很適合使用單例模式來實現,因為即便你的伺服器有很多種服務,但他們最後必須要透過有限的連線池來向資料庫提交儲存,其資料結構是被儲存體定義的。為了能夠妥善且高效的管理資源,集中起來透過統一的實例來執行是相對方便的。

在這邊提供一個封裝的目錄範例:

└─database
├─dbutlis \\共用的功能性組件
├─errorpkg \\定義或重新封裝需要特別處裡的錯誤
├─game \\遊戲相關的資料表結構與查詢封裝
│ ├─complexquery
│ ├─gamehistory
│ ├─handresult
│ ├─playerhistory
│ ├─ruleconfig
│ ├─tableconfig
│ ├─tablesetting
│ └─taxconfig
└─database.go \\初始化相關

若你的系統同時應用了兩種以上的rdbms,又或者是使用了不同的驅動來操作資料庫; 比如說大部分的CRUD使用GORM,但一些QPS過高的API會需要使用pgx,建議應該先以其種類分成不同的包,理由是資料表本身應是依賴於資料庫而存在,必定是會有一些功能機制會根據資料庫而有所差異,導致抽換資料庫於資料表的定義實際上變得窒礙難行。

├─mysql
│ ├─dbutlis \\共用的功能性組件
│ └─ ...
└─Postgres
├─dbutlis \\共用的功能性組件
└─ ...

資料表的部分,在良好的系統設計下,遊戲伺服器的資料表應是完全為了遊戲而存在,因此orm的封裝會被放在同個資料夾下,但如果有一些資料表跟遊戲邏輯無關的,請考慮依據功能分類,放置在另外的子目錄下,

└─database
├─game
│ └─...
├─transaction
| └─...
└─...

通常在golang的包底下,會約定俗成的有一個與包名相同的資料夾,你需要在裡面做一些連線相關的初始化,以及針對底下資料表的orm做依賴注入。 並且規模不大的時候,你也可以把整個模組的功能函式如 migrator , seeder 先丟這。

type Database struct {
db *gorm.DB
}

var db *Database = nil

func InitDatabase(loud bool) {
//prepare connection info
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s sslmode=disable dbname=%s", config.Database().Host, config.Database().Port, config.Database().Username, config.Database().Password, config.Database().Database)
//init gorm log for trace sql instruction
loggerConfig := gormLog.New(log.New(os.Stdout, "\n", log.LstdFlags), gormLog.Config{
LogLevel: gormLog.Silent, // Silent, Error, Warn, Info
Colorful: true,
})
if loud {
loggerConfig = gormLog.New(log.New(os.Stdout, "\n", log.LstdFlags), gormLog.Config{
LogLevel: gormLog.Info, // Silent, Error, Warn, Info
Colorful: true,
})
}
//init gorm connection
_db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
NowFunc: func() time.Time {
return time.Now().UTC()
},
Logger: loggerConfig,
})

if err != nil {
panic(err)
}

db = &Database{
db: _db,
}
//init Repository
handresult.InitRepository(_db)
playerhistory.InitRepository(_db)
gamehistory.InitRepository(_db)
tableconfig.InitRepository(_db)
ruleconfig.InitRepository(_db)
taxconfig.InitRepository(_db)
tablesetting.InitRepository(_db)
complexquery.InitRepository(_db)
transaction.InitRepository(_db)
}

由於我們會把資料表的增刪查改 CRUD封裝在一起,針對查詢的部分可能會有一些filter是能夠共用的,因此你可以將他們定義在dbutils下。

package dbutlis

import (
"mahjongserver/utils/json"
"gorm.io/gorm"
)

type Paginator struct {
Page int `json:"Page"`
PageSize int `json:"PageSize"`
}

func (p Paginator) Paginate(db *gorm.DB) *gorm.DB {
if p.Page < 1 {
p.Page = 1
}
if p.PageSize < 1 {
p.PageSize = 10
}
if p.PageSize > 100 {
p.PageSize = 100
}

offset := (p.Page - 1) * p.PageSize
return db.Offset(offset).Limit(p.PageSize)
}

而此套件在運作的過程中,也許會發生一些例外狀況是需要處裡的,或是你為了讓日誌器傾印的時能夠印出更加直觀的內容,則需要定義/重新封裝錯誤在errorpkg

package errorpkg

import "errors"
var (
ErrFailedToMigrate error = errors.New("failed to migrate")
ErrFailedToSeed error = errors.New("failed to seed")
ErrDatabaseNotInit error = errors.New("database not init")
)

var (
ErrPartitionSubTableExist error = errors.New("partition sub table exist")
)