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

259 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分表查询服务使用指南
## 概述
`ShardingQueryService` 是一个通用的分表查询服务,用于简化多分表查询的实现。它封装了 UNION ALL 查询、分页、排序等通用逻辑,让开发者只需要关注业务特定的筛选条件和列定义。
## 核心优势
1. **代码复用**:避免为每个分表重复编写 UNION ALL 查询逻辑
2. **统一优化**:所有分表查询都使用相同的优化策略(表存在性检查、列名明确指定等)
3. **易于维护**:通用逻辑集中管理,修改一处即可影响所有使用该服务的表
4. **类型安全**:通过接口和回调函数保证类型安全
## 使用步骤
### 1. 定义筛选条件结构体
```go
// 例如:日志表的筛选条件
type LogFilters struct {
UserID uint // 用户ID
Level string // 日志级别
Keyword string // 关键词搜索
StartTime time.Time // 开始时间
EndTime time.Time // 结束时间
OrderBy string // 排序字段(格式:字段:asc/desc)
}
```
### 2. 实现 WHERE 条件构建函数
```go
// 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. 实现列名获取函数
```go
// 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. 初始化分表查询服务
```go
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. 使用分表查询服务
```go
// 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` 中的实现,这是使用通用分表查询服务的完整示例。
## 优势对比
### 使用通用服务前(每个表都需要重复实现)
```go
// 每个表都需要实现这些方法
func (s *OrderServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
func (s *LogServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
func (s *PaymentServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
// ... 每个表都重复
```
### 使用通用服务后(只需配置)
```go
// 每个表只需要配置一次
service.shardingQueryService = NewShardingQueryService(ShardingQueryConfig{
// ... 配置
})
// 使用时只需一行代码
total, err := s.shardingQueryService.QueryMultipleTables(tableNames, filters, page, pageSize, &results)
```
## 扩展性
如果需要添加新的通用功能(如缓存、性能监控等),只需要在 `ShardingQueryService` 中修改一次,所有使用该服务的表都会自动获得这些功能。