Files
server/docs/SHARDING_QUERY_SERVICE.md
T
2026-01-16 15:49:34 +08:00

8.4 KiB
Raw Blame History

分表查询服务使用指南

概述

ShardingQueryService 是一个通用的分表查询服务,用于简化多分表查询的实现。它封装了 UNION ALL 查询、分页、排序等通用逻辑,让开发者只需要关注业务特定的筛选条件和列定义。

核心优势

  1. 代码复用:避免为每个分表重复编写 UNION ALL 查询逻辑
  2. 统一优化:所有分表查询都使用相同的优化策略(表存在性检查、列名明确指定等)
  3. 易于维护:通用逻辑集中管理,修改一处即可影响所有使用该服务的表
  4. 类型安全:通过接口和回调函数保证类型安全

使用步骤

1. 定义筛选条件结构体

// 例如:日志表的筛选条件
type LogFilters struct {
    UserID    uint      // 用户ID
    Level     string    // 日志级别
    Keyword   string    // 关键词搜索
    StartTime time.Time // 开始时间
    EndTime   time.Time // 结束时间
    OrderBy   string    // 排序字段(格式:字段:asc/desc)
}

2. 实现 WHERE 条件构建函数

// buildLogWhereClause 构建日志查询的 WHERE 条件
func (s *LogServiceImpl) buildLogWhereClause(filters any) (string, []any) {
    logFilters, ok := filters.(LogFilters)
    if !ok {
        return "", nil
    }

    var conditions []string
    var args []any

    // 时间范围
    if !logFilters.StartTime.IsZero() {
        conditions = append(conditions, "created_at >= ?")
        args = append(args, logFilters.StartTime)
    }
    if !logFilters.EndTime.IsZero() {
        conditions = append(conditions, "created_at <= ?")
        args = append(args, logFilters.EndTime)
    }

    // 用户ID筛选
    if logFilters.UserID > 0 {
        conditions = append(conditions, "user_id = ?")
        args = append(args, logFilters.UserID)
    }

    // 日志级别筛选
    if logFilters.Level != "" {
        conditions = append(conditions, "level = ?")
        args = append(args, logFilters.Level)
    }

    // 关键词搜索
    if logFilters.Keyword != "" {
        conditions = append(conditions, "(message LIKE ? OR context LIKE ?)")
        keyword := "%" + logFilters.Keyword + "%"
        args = append(args, keyword, keyword)
    }

    return strings.Join(conditions, " AND "), args
}

3. 实现列名获取函数

// getLogTableColumns 获取日志表的所有列名
func (s *LogServiceImpl) getLogTableColumns() string {
    // 只包含模型中定义的字段,确保所有分表都有这些字段
    columns := []string{
        "id",
        "user_id",
        "level",
        "message",
        "context",
        "created_at",
        "updated_at",
        "deleted_at",
    }
    return strings.Join(columns, ", ")
}

4. 初始化分表查询服务

type LogServiceImpl struct {
    shardingService      ShardingService
    shardingQueryService ShardingQueryService
}

func NewLogService() *LogServiceImpl {
    service := &LogServiceImpl{
        shardingService: NewShardingService(),
    }

    // 初始化分表查询服务
    service.shardingQueryService = NewShardingQueryService(ShardingQueryConfig{
        BaseTableName: "logs",
        GetColumns: func() string {
            return service.getLogTableColumns()
        },
        BuildWhereClause: func(filters any) (string, []any) {
            return service.buildLogWhereClause(filters)
        },
        GetAllowedOrderFields: func() map[string]bool {
            return map[string]bool{
                "id":         true,
                "user_id":    true,
                "level":       true,
                "created_at": true,
                "updated_at": true,
            }
        },
        DefaultOrderBy: "created_at:desc",
        ModuleName:     "log",
    })

    return service
}

5. 使用分表查询服务

// GetLogs 查询日志列表(支持多分表)
func (s *LogServiceImpl) GetLogs(filters LogFilters, page, pageSize int) ([]models.Log, int64, error) {
    // 验证时间范围
    valid, err := utils.ValidateTimeRange(filters.StartTime, filters.EndTime)
    if !valid {
        return nil, 0, err
    }

    // 获取需要查询的所有分表
    tableNames := utils.GetShardingTableNames("logs", filters.StartTime, filters.EndTime)
    if len(tableNames) == 0 {
        return []models.Log{}, 0, nil
    }

    // 如果只有一个分表,直接查询(使用 ORM)
    if len(tableNames) == 1 {
        return s.querySingleTable(tableNames[0], filters, page, pageSize)
    }

    // 多个分表:使用通用分表查询服务
    var logs []models.Log
    total, err := s.shardingQueryService.QueryMultipleTables(tableNames, filters, page, pageSize, &logs)
    if err != nil {
        return nil, 0, err
    }
    return logs, total, nil
}

// GetAllLogsForExport 获取所有日志用于导出
func (s *LogServiceImpl) GetAllLogsForExport(filters LogFilters) ([]models.Log, error) {
    // 验证时间范围
    valid, err := utils.ValidateTimeRange(filters.StartTime, filters.EndTime)
    if !valid {
        return nil, err
    }

    // 获取需要查询的所有分表
    tableNames := utils.GetShardingTableNames("logs", filters.StartTime, filters.EndTime)
    if len(tableNames) == 0 {
        return []models.Log{}, nil
    }

    // 如果只有一个分表,直接查询
    if len(tableNames) == 1 {
        query := s.buildShardingQuery(tableNames[0], filters)
        orderBy := filters.OrderBy
        if orderBy == "" {
            orderBy = "created_at:desc"
        }
        query = s.applyOrderBy(query, orderBy)

        var logs []models.Log
        if err := query.Find(&logs); err != nil {
            return nil, apperrors.ErrQueryFailed.WithError(err)
        }
        return logs, nil
    }

    // 多个分表:使用通用分表查询服务
    var logs []models.Log
    err = s.shardingQueryService.QueryMultipleTablesForExport(tableNames, filters, &logs)
    if err != nil {
        return nil, err
    }
    return logs, nil
}

配置说明

ShardingQueryConfig 字段说明

字段 类型 说明 必填
BaseTableName string 基础表名,如 "orders"
GetColumns func() string 获取表的所有列名(用于 UNION ALL 查询)
BuildWhereClause func(any) (string, []any) 构建 WHERE 条件,返回条件字符串和参数列表
GetAllowedOrderFields func() map[string]bool 获取允许排序的字段列表
DefaultOrderBy string 默认排序,格式:字段:方向,如 "created_at:desc" 否(默认:created_at:desc
ModuleName string 模块名称,用于日志记录 否(默认:sharding

注意事项

  1. 列名必须明确指定:不要使用 SELECT *,必须明确列出所有列名,确保不同分表的列数一致
  2. 只包含模型中的字段:列名列表应该只包含模型中定义的字段,避免查询不存在的字段
  3. WHERE 条件格式BuildWhereClause 返回的条件字符串不应该包含 WHERE 关键字,只返回条件部分(如 "user_id = ? AND status = ?"
  4. 时间范围处理:如果筛选条件包含时间范围,应该在调用 QueryMultipleTables 之前验证时间范围
  5. 单表优化:如果只有一个分表,建议直接使用 ORM 查询,而不是使用 UNION ALL(性能更好)

完整示例

参考 app/services/order_service.go 中的实现,这是使用通用分表查询服务的完整示例。

优势对比

使用通用服务前(每个表都需要重复实现)

// 每个表都需要实现这些方法
func (s *OrderServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
func (s *LogServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
func (s *PaymentServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
// ... 每个表都重复

使用通用服务后(只需配置)

// 每个表只需要配置一次
service.shardingQueryService = NewShardingQueryService(ShardingQueryConfig{
    // ... 配置
})

// 使用时只需一行代码
total, err := s.shardingQueryService.QueryMultipleTables(tableNames, filters, page, pageSize, &results)

扩展性

如果需要添加新的通用功能(如缓存、性能监控等),只需要在 ShardingQueryService 中修改一次,所有使用该服务的表都会自动获得这些功能。