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

4 回复

这里的帖子通常都得不到回复吗?我想了解一下这个论坛是否还在活跃。

更多关于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通信
  • 转换函数负责模型间的映射
回到顶部