249 lines
7.2 KiB
Markdown
249 lines
7.2 KiB
Markdown
|
|
---
|
|||
|
|
name: 多数据库方言与前缀支持
|
|||
|
|
overview: 为 HoTimeDB ORM 实现完整的多数据库(MySQL/PostgreSQL/SQLite)方言支持和自动表前缀功能,采用智能解析+辅助方法兜底的混合策略,保持完全向后兼容。
|
|||
|
|
todos:
|
|||
|
|
- id: dialect-interface
|
|||
|
|
content: 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
|
|||
|
|
status: pending
|
|||
|
|
- id: identifier-processor
|
|||
|
|
content: 新建 identifier.go,实现 IdentifierProcessor 及智能解析逻辑
|
|||
|
|
status: pending
|
|||
|
|
- id: db-integration
|
|||
|
|
content: 在 db.go 中集成处理器,添加 T() 和 C() 辅助方法
|
|||
|
|
status: pending
|
|||
|
|
- id: crud-update
|
|||
|
|
content: 修改 crud.go 中 Select/Insert/Update/Delete/buildJoin 等方法
|
|||
|
|
status: pending
|
|||
|
|
- id: where-update
|
|||
|
|
content: 修改 where.go 中 varCond 等条件处理方法
|
|||
|
|
status: pending
|
|||
|
|
- id: builder-check
|
|||
|
|
content: 检查 builder.go 是否需要额外修改
|
|||
|
|
status: pending
|
|||
|
|
- id: testing
|
|||
|
|
content: 编写测试用例验证多数据库和前缀功能
|
|||
|
|
status: pending
|
|||
|
|
- id: todo-1769037903242-d7aip6nh1
|
|||
|
|
content: ""
|
|||
|
|
status: pending
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# HoTimeDB 多数据库方言与自动前缀支持计划(更新版)
|
|||
|
|
|
|||
|
|
## 目标
|
|||
|
|
|
|||
|
|
1. **多数据库方言支持**:MySQL、PostgreSQL、SQLite 标识符引号自动转换
|
|||
|
|
2. **自动表前缀**:主表、JOIN 表、ON/WHERE 条件中的表名自动添加前缀
|
|||
|
|
3. **完全向后兼容**:用户现有写法无需修改
|
|||
|
|
4. **辅助方法兜底**:边缘情况可用 `T()` / `C()` 精确控制
|
|||
|
|
|
|||
|
|
## 混合策略设计
|
|||
|
|
|
|||
|
|
### 各部分处理方式
|
|||
|
|
|
|||
|
|
| 位置 | 处理方式 | 准确度 | 说明 |
|
|||
|
|
|
|||
|
|
|------|---------|--------|------|
|
|||
|
|
|
|||
|
|
| 主表名 | 自动 | 100% | `Select("order")` 自动处理 |
|
|||
|
|
|
|||
|
|
| JOIN 表名 | 自动 | 100% | `[><]order` 中提取表名处理 |
|
|||
|
|
|
|||
|
|
| ON 条件字符串 | 智能解析 | ~95% | 正则匹配 `table.column` 模式 |
|
|||
|
|
|
|||
|
|
| WHERE 条件 Map | 自动 | 100% | Map 的 key 是结构化的 |
|
|||
|
|
|
|||
|
|
| SELECT 字段 | 智能解析 | ~95% | 同 ON 条件 |
|
|||
|
|
|
|||
|
|
### 辅助方法(兜底)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
db.T("order") // 返回 "`app_order`" (MySQL) 或 "\"app_order\"" (PG)
|
|||
|
|
db.C("order", "name") // 返回 "`app_order`.`name`"
|
|||
|
|
db.C("order.name") // 同上,支持点号格式
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实现步骤
|
|||
|
|
|
|||
|
|
### 第1步:扩展 Dialect 接口([db/dialect.go](db/dialect.go))
|
|||
|
|
|
|||
|
|
添加新方法到 `Dialect` 接口:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// QuoteIdentifier 处理单个标识符(去除已有引号,添加正确引号)
|
|||
|
|
QuoteIdentifier(name string) string
|
|||
|
|
|
|||
|
|
// QuoteChar 返回引号字符
|
|||
|
|
QuoteChar() string
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
三种方言实现:
|
|||
|
|
|
|||
|
|
- MySQL: 反引号 `` ` ``
|
|||
|
|
- PostgreSQL/SQLite: 双引号 `"`
|
|||
|
|
|
|||
|
|
### 第2步:添加标识符处理器([db/identifier.go](db/identifier.go) 新文件)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type IdentifierProcessor struct {
|
|||
|
|
dialect Dialect
|
|||
|
|
prefix string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ProcessTableName 处理表名(添加前缀+引号)
|
|||
|
|
// "order" → "`app_order`"
|
|||
|
|
func (p *IdentifierProcessor) ProcessTableName(name string) string
|
|||
|
|
|
|||
|
|
// ProcessColumn 处理 table.column 格式
|
|||
|
|
// "order.name" → "`app_order`.`name`"
|
|||
|
|
// "`order`.name" → "`app_order`.`name`"
|
|||
|
|
func (p *IdentifierProcessor) ProcessColumn(name string) string
|
|||
|
|
|
|||
|
|
// ProcessConditionString 智能解析条件字符串
|
|||
|
|
// "user.id = order.user_id" → "`app_user`.`id` = `app_order`.`user_id`"
|
|||
|
|
func (p *IdentifierProcessor) ProcessConditionString(condition string) string
|
|||
|
|
|
|||
|
|
// ProcessFieldList 处理字段列表字符串
|
|||
|
|
// "order.id, user.name AS uname" → "`app_order`.`id`, `app_user`.`name` AS uname"
|
|||
|
|
func (p *IdentifierProcessor) ProcessFieldList(fields string) string
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**智能解析正则**:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 匹配 table.column 模式,排除已有引号、函数调用等
|
|||
|
|
// 模式: \b([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\b
|
|||
|
|
// 排除: `table`.column, "table".column, FUNC(), 123.456
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第3步:在 HoTimeDB 中集成([db/db.go](db/db.go))
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// processor 缓存
|
|||
|
|
var processorOnce sync.Once
|
|||
|
|
var processor *IdentifierProcessor
|
|||
|
|
|
|||
|
|
// GetProcessor 获取标识符处理器
|
|||
|
|
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor
|
|||
|
|
|
|||
|
|
// T 辅助方法:获取带前缀和引号的表名
|
|||
|
|
func (that *HoTimeDB) T(table string) string
|
|||
|
|
|
|||
|
|
// C 辅助方法:获取带前缀和引号的 table.column
|
|||
|
|
func (that *HoTimeDB) C(args ...string) string
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 第4步:修改 CRUD 方法([db/crud.go](db/crud.go))
|
|||
|
|
|
|||
|
|
**Select 方法改动**:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// L112-116 原代码
|
|||
|
|
if !strings.Contains(table, ".") && !strings.Contains(table, " AS ") {
|
|||
|
|
query += " FROM `" + that.Prefix + table + "` "
|
|||
|
|
} else {
|
|||
|
|
query += " FROM " + that.Prefix + table + " "
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 改为
|
|||
|
|
query += " FROM " + that.GetProcessor().ProcessTableName(table) + " "
|
|||
|
|
|
|||
|
|
// 字段列表处理(L90-107)
|
|||
|
|
// 如果是字符串,调用 ProcessFieldList 处理
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**buildJoin 方法改动**(L156-222):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 原代码 L186-190
|
|||
|
|
table := Substr(k, 3, len(k)-3)
|
|||
|
|
if !strings.Contains(table, " ") {
|
|||
|
|
table = "`" + table + "`"
|
|||
|
|
}
|
|||
|
|
query += " LEFT JOIN " + table + " ON " + v.(string) + " "
|
|||
|
|
|
|||
|
|
// 改为
|
|||
|
|
table := Substr(k, 3, len(k)-3)
|
|||
|
|
table = that.GetProcessor().ProcessTableName(table)
|
|||
|
|
onCondition := that.GetProcessor().ProcessConditionString(v.(string))
|
|||
|
|
query += " LEFT JOIN " + table + " ON " + onCondition + " "
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Insert/BatchInsert/Update/Delete** 同样修改表名和字段名处理。
|
|||
|
|
|
|||
|
|
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
|||
|
|
|
|||
|
|
**varCond 方法改动**(多处):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 原代码(多处出现)
|
|||
|
|
if !strings.Contains(k, ".") {
|
|||
|
|
k = "`" + k + "`"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 改为
|
|||
|
|
k = that.GetProcessor().ProcessColumn(k)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
需要修改的函数:
|
|||
|
|
|
|||
|
|
- `varCond` (L205-338)
|
|||
|
|
- `handleDefaultCondition` (L340-368)
|
|||
|
|
- `handlePlainField` (L370-400)
|
|||
|
|
|
|||
|
|
### 第6步:修改链式构建器([db/builder.go](db/builder.go))
|
|||
|
|
|
|||
|
|
**LeftJoin 等方法需要传递处理器**:
|
|||
|
|
|
|||
|
|
由于 builder 持有 HoTimeDB 引用,可以直接使用:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
|||
|
|
// 不在这里处理,让 buildJoin 统一处理
|
|||
|
|
that.Join(Map{"[>]" + table: joinStr})
|
|||
|
|
return that
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
JOIN 的实际处理在 `crud.go` 的 `buildJoin` 中完成。
|
|||
|
|
|
|||
|
|
## 智能解析的边界处理
|
|||
|
|
|
|||
|
|
### 会自动处理的情况
|
|||
|
|
|
|||
|
|
- `user.id = order.user_id` → 正确处理
|
|||
|
|
- `user.id=order.user_id` → 正确处理(无空格)
|
|||
|
|
- `` `user`.id = order.user_id `` → 正确处理(混合格式)
|
|||
|
|
- `user.id = order.user_id AND order.status = 1` → 正确处理
|
|||
|
|
|
|||
|
|
### 需要辅助方法的边缘情况
|
|||
|
|
|
|||
|
|
- 子查询中的表名
|
|||
|
|
- 复杂 CASE WHEN 表达式
|
|||
|
|
- 动态拼接的 SQL 片段
|
|||
|
|
|
|||
|
|
## 文件清单
|
|||
|
|
|
|||
|
|
| 文件 | 操作 | 说明 |
|
|||
|
|
|
|||
|
|
|------|------|------|
|
|||
|
|
|
|||
|
|
| [db/dialect.go](db/dialect.go) | 修改 | 扩展 Dialect 接口 |
|
|||
|
|
|
|||
|
|
| [db/identifier.go](db/identifier.go) | 新增 | IdentifierProcessor 实现 |
|
|||
|
|
|
|||
|
|
| [db/db.go](db/db.go) | 修改 | 集成处理器,添加 T()/C() 方法 |
|
|||
|
|
|
|||
|
|
| [db/crud.go](db/crud.go) | 修改 | 修改所有 CRUD 方法 |
|
|||
|
|
|
|||
|
|
| [db/where.go](db/where.go) | 修改 | 修改条件处理逻辑 |
|
|||
|
|
|
|||
|
|
| [db/builder.go](db/builder.go) | 检查 | 可能无需修改(buildJoin 统一处理)|
|
|||
|
|
|
|||
|
|
## 测试用例
|
|||
|
|
|
|||
|
|
1. **多数据库切换**:MySQL → PostgreSQL → SQLite
|
|||
|
|
2. **前缀场景**:有前缀 vs 无前缀
|
|||
|
|
3. **复杂 JOIN**:多表 JOIN + 复杂 ON 条件
|
|||
|
|
4. **混合写法**:`order.name` + `` `user`.id `` 混用
|
|||
|
|
5. **辅助方法**:`T()` 和 `C()` 正确性
|