Golang中如何根据领域模型设计数据库模型
Golang中如何根据领域模型设计数据库模型 我想提升我的Go技能,并考虑创建一个简单的扫雷游戏后端。对于领域模型,我最初是这样设计的:
import "github.com/google/uuid"
type Game struct {
ID uuid.UUID
Board [][]*BoardCell
AmountOfMines uint8
}
type BoardCell struct {
IsRevealed bool
HasBeenRevealedAfterEndOfGame bool
HasFlag bool
HasMine bool
AmountOfNeighbourMines uint8
}
通过这些信息,你可以检查整个游戏棋盘,同时跟踪AmountOfNeighbourMines字段仅出于性能考虑(因为我们只需要计算一次)。
我想知道你会如何为此设计数据库模型。
我个人认为我会使用与SQL表相同的结构: Game => ID, AmountOfMines BoardCell => gameID (外键), …字段…
但当涉及到数据传输对象(DTO)时,我不确定是否应该直接复制粘贴领域模型。如果是这样的话,内存数据库可以在一个映射[gameID, game]中跟踪游戏,但也许在数据库层有更好的方式来管理游戏。
提前感谢。
更多关于Golang中如何根据领域模型设计数据库模型的实战教程也可以访问 https://www.itying.com/category-94-b0.html
这里的帖子通常都得不到回复吗?我想了解一下这个论坛是否还在活跃。
更多关于Golang中如何根据领域模型设计数据库模型的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我在这里。😊
关于你的问题,我不完全确定我理解你所问的一切。然而,我是从数据库的角度来考虑设计的。
我理解你需要在某个时刻保存 AmountOfNeighbourMines 以节省处理能力,但我更倾向于不将其包含在数据库中。我猜你想在某个特定时刻保存它,对吗?所以性能并不是那么重要,而且所有信息都在那里。
你没有提供太多信息,但我觉得人们通常为 Go 使用了太多的面向对象编程(OOP)。传输一个数据传输对象(DTO)感觉没有必要地复杂。也许最好直接发送所需的数据,并以此思路来思考。
再次强调,没有更多细节很难确定,但我的直觉告诉我,你的代码可能至少有一到两个不必要的复杂层,更像是 Java 或 Cpp 的风格。
我认为我不会将诸如AmountOfMines(地雷数量)这样的信息存储在我的game表中,因为它是可以计算得出的。也许可以从类似下面的结构开始(顺便说一句,我是在记事本里打的,语法可能稍有错误,而且我目前习惯用MariaDB,所以这个示例是MySQL风格的):
-- 创建游戏表
CREATE TABLE games (
id int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
);
-- 创建游戏单元格表
CREATE TABLE game_cells (
id int(11) NOT NULL AUTO_INCREMENT,
game_id int(11) NOT NULL,
x int(11) NOT NULL,
y int(11) NOT NULL,
is_revealed tinyint(1) NOT NULL,
is_flagged tinyint(1) NOT NULL,
is_mine tinyint(1) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (game_id) REFERENCES games(id)
);
总之,我想表达的意思是:你可以轻松地构建出像AmountOfMines这样的信息:
select
count(id) as num_cells,
sum(is_mine) as num_mines
from game_cells where game_id = 1;
在Go中根据领域模型设计数据库模型时,通常采用分层架构,保持领域模型的纯净性,同时创建专门的数据库模型和DTO。以下是一个针对扫雷游戏的设计方案:
1. 数据库模型设计
// 数据库模型
type GameDB struct {
ID string `gorm:"primaryKey;type:uuid"`
Width int `gorm:"not null"`
Height int `gorm:"not null"`
AmountOfMines int `gorm:"not null"`
Status string `gorm:"type:varchar(20);not null"` // playing, won, lost
CreatedAt time.Time
UpdatedAt time.Time
}
type BoardCellDB struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
GameID string `gorm:"type:uuid;not null;index"`
Row int `gorm:"not null"`
Col int `gorm:"not null"`
IsRevealed bool `gorm:"not null;default:false"`
HasFlag bool `gorm:"not null;default:false"`
HasMine bool `gorm:"not null;default:false"`
AmountOfNeighbourMines int `gorm:"not null;default:0"`
// 添加索引以提高查询性能
gorm.Model
}
// 数据库迁移示例
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(&GameDB{}, &BoardCellDB{})
}
2. 领域模型保持不变
// 领域模型 - 保持业务逻辑纯净
type Game struct {
ID uuid.UUID
Board [][]*BoardCell
AmountOfMines uint8
Width int
Height int
Status string // playing, won, lost
}
type BoardCell struct {
IsRevealed bool
HasBeenRevealedAfterEndOfGame bool
HasFlag bool
HasMine bool
AmountOfNeighbourMines uint8
}
3. 数据传输对象(DTO)
// 请求DTO
type CreateGameRequest struct {
Width int `json:"width" validate:"required,min=5,max=30"`
Height int `json:"height" validate:"required,min=5,max=30"`
AmountOfMines int `json:"amountOfMines" validate:"required,min=1"`
}
type RevealCellRequest struct {
GameID string `json:"gameId" validate:"required,uuid4"`
Row int `json:"row" validate:"required,min=0"`
Col int `json:"col" validate:"required,min=0"`
}
// 响应DTO
type GameResponse struct {
ID string `json:"id"`
Width int `json:"width"`
Height int `json:"height"`
AmountOfMines int `json:"amountOfMines"`
Status string `json:"status"`
Board [][]CellResponse `json:"board"`
}
type CellResponse struct {
IsRevealed bool `json:"isRevealed"`
HasFlag bool `json:"hasFlag"`
HasMine bool `json:"hasMine,omitempty"` // 游戏未结束时隐藏
AmountOfNeighbourMines int `json:"amountOfNeighbourMines,omitempty"`
}
4. 转换函数示例
// 领域模型到数据库模型的转换
func (g *Game) ToDBModel() (*GameDB, []*BoardCellDB) {
gameDB := &GameDB{
ID: g.ID.String(),
Width: g.Width,
Height: g.Height,
AmountOfMines: int(g.AmountOfMines),
Status: g.Status,
}
var cellsDB []*BoardCellDB
for i, row := range g.Board {
for j, cell := range row {
if cell != nil {
cellDB := &BoardCellDB{
GameID: g.ID.String(),
Row: i,
Col: j,
IsRevealed: cell.IsRevealed,
HasFlag: cell.HasFlag,
HasMine: cell.HasMine,
AmountOfNeighbourMines: int(cell.AmountOfNeighbourMines),
}
cellsDB = append(cellsDB, cellDB)
}
}
}
return gameDB, cellsDB
}
// 数据库模型到领域模型的转换
func FromDBModel(gameDB *GameDB, cellsDB []*BoardCellDB) *Game {
game := &Game{
ID: uuid.MustParse(gameDB.ID),
Width: gameDB.Width,
Height: gameDB.Height,
AmountOfMines: uint8(gameDB.AmountOfMines),
Status: gameDB.Status,
}
// 初始化棋盘
board := make([][]*BoardCell, gameDB.Height)
for i := range board {
board[i] = make([]*BoardCell, gameDB.Width)
}
// 填充单元格
for _, cellDB := range cellsDB {
if cellDB.Row < gameDB.Height && cellDB.Col < gameDB.Width {
board[cellDB.Row][cellDB.Col] = &BoardCell{
IsRevealed: cellDB.IsRevealed,
HasFlag: cellDB.HasFlag,
HasMine: cellDB.HasMine,
AmountOfNeighbourMines: uint8(cellDB.AmountOfNeighbourMines),
}
}
}
game.Board = board
return game
}
5. 存储库层实现
type GameRepository interface {
Create(game *Game) error
FindByID(id uuid.UUID) (*Game, error)
Update(game *Game) error
Delete(id uuid.UUID) error
}
type PostgresGameRepository struct {
db *gorm.DB
}
func (r *PostgresGameRepository) Create(game *Game) error {
gameDB, cellsDB := game.ToDBModel()
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(gameDB).Error; err != nil {
return err
}
if len(cellsDB) > 0 {
if err := tx.CreateInBatches(cellsDB, 100).Error; err != nil {
return err
}
}
return nil
})
}
func (r *PostgresGameRepository) FindByID(id uuid.UUID) (*Game, error) {
var gameDB GameDB
if err := r.db.Where("id = ?", id.String()).First(&gameDB).Error; err != nil {
return nil, err
}
var cellsDB []BoardCellDB
if err := r.db.Where("game_id = ?", id.String()).Find(&cellsDB).Error; err != nil {
return nil, err
}
// 转换为领域模型
cellsPtrs := make([]*BoardCellDB, len(cellsDB))
for i := range cellsDB {
cellsPtrs[i] = &cellsDB[i]
}
return FromDBModel(&gameDB, cellsPtrs), nil
}
6. 服务层示例
type GameService struct {
repo GameRepository
}
func (s *GameService) CreateGame(req CreateGameRequest) (*GameResponse, error) {
// 创建领域模型
game := &Game{
ID: uuid.New(),
Width: req.Width,
Height: req.Height,
AmountOfMines: uint8(req.AmountOfMines),
Status: "playing",
}
// 初始化棋盘逻辑
game.Board = initializeBoard(game.Width, game.Height, game.AmountOfMines)
// 保存到数据库
if err := s.repo.Create(game); err != nil {
return nil, err
}
// 转换为响应DTO
return s.toGameResponse(game, false), nil
}
func (s *GameService) toGameResponse(game *Game, showAll bool) *GameResponse {
response := &GameResponse{
ID: game.ID.String(),
Width: game.Width,
Height: game.Height,
AmountOfMines: int(game.AmountOfMines),
Status: game.Status,
Board: make([][]CellResponse, game.Height),
}
for i := 0; i < game.Height; i++ {
response.Board[i] = make([]CellResponse, game.Width)
for j := 0; j < game.Width; j++ {
cell := game.Board[i][j]
cellResp := CellResponse{
IsRevealed: cell.IsRevealed,
HasFlag: cell.HasFlag,
}
// 根据游戏状态决定是否显示地雷
if showAll || cell.IsRevealed || game.Status != "playing" {
cellResp.HasMine = cell.HasMine
cellResp.AmountOfNeighbourMines = int(cell.AmountOfNeighbourMines)
}
response.Board[i][j] = cellResp
}
}
return response
}
这个设计保持了关注点分离:
- 领域模型专注于业务逻辑
- 数据库模型优化存储结构
- DTO处理API通信
- 转换函数负责模型间的映射

