feat(db): 实现多数据库方言与自动表前缀支持
- 扩展 Dialect 接口添加 QuoteIdentifier 和 QuoteChar 方法 - 实现 IdentifierProcessor 结构体处理标识符转换 - 添加系统数据库列表避免对 INFORMATION_SCHEMA 等添加前缀 - 支持 database.table 格式的表名处理和前缀添加 - 在 CRUD 方法中集成新的标识符处理器 - 修改 WHERE 条件处理逻辑支持自动前缀 - 更新链式构建器的 JOIN 方法处理表名和条件 - 保持完全向后兼容性支持现有写法
This commit is contained in:
parent
650fafad1a
commit
29a3b6095d
@ -1,208 +0,0 @@
|
|||||||
---
|
|
||||||
name: 多数据库方言与前缀支持
|
|
||||||
overview: 为 HoTimeDB ORM 实现完整的多数据库(MySQL/PostgreSQL/SQLite)方言支持和自动表前缀功能,同时保持完全向后兼容。
|
|
||||||
todos:
|
|
||||||
- id: dialect-interface
|
|
||||||
content: 扩展 Dialect 接口,添加 QuoteIdentifier 和 QuoteChar 方法
|
|
||||||
status: pending
|
|
||||||
- id: identifier-processor
|
|
||||||
content: 实现 IdentifierProcessor 结构体及其方法
|
|
||||||
status: pending
|
|
||||||
- id: db-integration
|
|
||||||
content: 在 HoTimeDB 中集成处理器,添加辅助方法
|
|
||||||
status: pending
|
|
||||||
- id: crud-update
|
|
||||||
content: 修改 crud.go 中的所有 CRUD 方法使用新处理器
|
|
||||||
status: pending
|
|
||||||
- id: where-update
|
|
||||||
content: 修改 where.go 中的条件处理逻辑
|
|
||||||
status: pending
|
|
||||||
- id: builder-update
|
|
||||||
content: 修改 builder.go 中的链式 JOIN 方法
|
|
||||||
status: pending
|
|
||||||
- id: testing
|
|
||||||
content: 测试多数据库和前缀功能
|
|
||||||
status: pending
|
|
||||||
---
|
|
||||||
|
|
||||||
# HoTimeDB 多数据库方言与自动前缀支持计划
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
1. **多数据库方言支持**:让所有 ORM 方法正确支持 MySQL、PostgreSQL、SQLite 的标识符引号格式
|
|
||||||
2. **自动表前缀**:在主表、JOIN 表、WHERE/ON 条件中自动识别并添加表前缀
|
|
||||||
3. **完全向后兼容**:用户现有写法(`order.name`、`` `order`.name ``)无需修改
|
|
||||||
|
|
||||||
## 核心设计
|
|
||||||
|
|
||||||
### 标识符处理流程
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
Input["用户输入: order.name 或 `order`.name"]
|
|
||||||
Parse["解析标识符"]
|
|
||||||
AddPrefix["添加表前缀"]
|
|
||||||
QuoteByDialect["根据数据库类型添加引号"]
|
|
||||||
Output["输出: `app_order`.`name` (MySQL) 或 \"app_order\".\"name\" (PG)"]
|
|
||||||
|
|
||||||
Input --> Parse
|
|
||||||
Parse --> AddPrefix
|
|
||||||
AddPrefix --> QuoteByDialect
|
|
||||||
QuoteByDialect --> Output
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现步骤
|
|
||||||
|
|
||||||
### 第1步:扩展 Dialect 接口([db/dialect.go](db/dialect.go))
|
|
||||||
|
|
||||||
在现有 `Dialect` 接口中添加新方法:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// QuoteIdentifier 处理标识符(支持 table.column 格式)
|
|
||||||
// 输入: "order" 或 "order.name" 或 "`order`.name"
|
|
||||||
// 输出: 带正确引号的标识符
|
|
||||||
QuoteIdentifier(name string) string
|
|
||||||
|
|
||||||
// QuoteChar 获取引号字符(用于字符串中的替换检测)
|
|
||||||
QuoteChar() string
|
|
||||||
```
|
|
||||||
|
|
||||||
为三种数据库实现这些方法,核心逻辑:
|
|
||||||
|
|
||||||
- 去除已有的引号(支持反引号和双引号)
|
|
||||||
- 按点号分割,对每部分单独加引号
|
|
||||||
- MySQL 使用反引号,PostgreSQL/SQLite 使用双引号
|
|
||||||
|
|
||||||
### 第2步:添加标识符处理器([db/dialect.go](db/dialect.go))
|
|
||||||
|
|
||||||
新增 `IdentifierProcessor` 结构体:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type IdentifierProcessor struct {
|
|
||||||
dialect Dialect
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessTableName 处理表名(添加前缀+引号)
|
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string
|
|
||||||
|
|
||||||
// ProcessColumn 处理字段名(table.column 格式,自动给表名加前缀)
|
|
||||||
func (p *IdentifierProcessor) ProcessColumn(name string) string
|
|
||||||
|
|
||||||
// ProcessCondition 处理 ON/WHERE 条件字符串中的 table.column
|
|
||||||
func (p *IdentifierProcessor) ProcessCondition(condition string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
关键实现细节:
|
|
||||||
|
|
||||||
- 使用正则表达式识别条件字符串中的 `table.column` 模式
|
|
||||||
- 识别已有的引号包裹(`` `table`.column `` 或 `"table".column`)
|
|
||||||
- 避免误处理字符串字面量中的内容
|
|
||||||
|
|
||||||
### 第3步:在 HoTimeDB 中集成处理器([db/db.go](db/db.go))
|
|
||||||
|
|
||||||
```go
|
|
||||||
// GetProcessor 获取标识符处理器(懒加载)
|
|
||||||
func (that *HoTimeDB) GetProcessor() *IdentifierProcessor
|
|
||||||
|
|
||||||
// processTable 内部方法:处理表名
|
|
||||||
func (that *HoTimeDB) processTable(table string) string
|
|
||||||
|
|
||||||
// processColumn 内部方法:处理字段名
|
|
||||||
func (that *HoTimeDB) processColumn(column string) string
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第4步:修改 CRUD 方法([db/crud.go](db/crud.go))
|
|
||||||
|
|
||||||
需要修改的方法及位置:
|
|
||||||
|
|
||||||
| 方法 | 修改内容 |
|
|
||||||
|
|
||||||
|------|---------|
|
|
||||||
|
|
||||||
| `Select` (L77-153) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `buildJoin` (L156-222) | JOIN 表名、ON 条件处理 |
|
|
||||||
|
|
||||||
| `Insert` (L248-302) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `BatchInsert` (L316-388) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `Update` (L598-638) | 表名、字段名处理 |
|
|
||||||
|
|
||||||
| `Delete` (L640-661) | 表名处理 |
|
|
||||||
|
|
||||||
核心改动模式(以 Select 为例):
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 之前
|
|
||||||
query += " FROM `" + that.Prefix + table + "` "
|
|
||||||
// 之后
|
|
||||||
query += " FROM " + that.processTable(table) + " "
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第5步:修改 WHERE 条件处理([db/where.go](db/where.go))
|
|
||||||
|
|
||||||
修改 `varCond` 方法(L205-338)中的字段名处理:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 之前
|
|
||||||
if !strings.Contains(k, ".") {
|
|
||||||
k = "`" + k + "`"
|
|
||||||
}
|
|
||||||
// 之后
|
|
||||||
k = that.processColumn(k)
|
|
||||||
```
|
|
||||||
|
|
||||||
同样修改 `handlePlainField`、`handleDefaultCondition` 等方法。
|
|
||||||
|
|
||||||
### 第6步:修改链式构建器([db/builder.go](db/builder.go))
|
|
||||||
|
|
||||||
`LeftJoin`、`RightJoin` 等方法中的表名和条件字符串也需要处理:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (that *HotimeDBBuilder) LeftJoin(table, joinStr string) *HotimeDBBuilder {
|
|
||||||
// 处理表名和 ON 条件
|
|
||||||
table = that.HoTimeDB.processTable(table)
|
|
||||||
joinStr = that.HoTimeDB.GetProcessor().ProcessCondition(joinStr)
|
|
||||||
that.Join(Map{"[>]" + table: joinStr})
|
|
||||||
return that
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安全考虑
|
|
||||||
|
|
||||||
1. **避免误替换数据**:条件处理只处理 SQL 语法结构中的标识符,不处理:
|
|
||||||
|
|
||||||
- 字符串字面量内的内容(通过检测引号边界)
|
|
||||||
- 已经是占位符 `?` 的参数值
|
|
||||||
|
|
||||||
2. **正则表达式设计**:识别 `table.column` 模式时排除:
|
|
||||||
|
|
||||||
- 数字开头的标识符
|
|
||||||
- 函数调用如 `NOW()`
|
|
||||||
- 特殊运算符如 `>=`、`<=`
|
|
||||||
|
|
||||||
## 测试要点
|
|
||||||
|
|
||||||
1. 多数据库类型切换
|
|
||||||
2. 带前缀和不带前缀场景
|
|
||||||
3. 复杂 JOIN 查询
|
|
||||||
4. 嵌套条件查询
|
|
||||||
5. 特殊字符和保留字作为表名/字段名
|
|
||||||
|
|
||||||
## 文件修改清单
|
|
||||||
|
|
||||||
| 文件 | 修改类型 |
|
|
||||||
|
|
||||||
|------|---------|
|
|
||||||
|
|
||||||
| [db/dialect.go](db/dialect.go) | 扩展接口,添加 IdentifierProcessor |
|
|
||||||
|
|
||||||
| [db/db.go](db/db.go) | 添加 GetProcessor 和辅助方法 |
|
|
||||||
|
|
||||||
| [db/crud.go](db/crud.go) | 修改所有 CRUD 方法 |
|
|
||||||
|
|
||||||
| [db/where.go](db/where.go) | 修改条件处理逻辑 |
|
|
||||||
|
|
||||||
| [db/builder.go](db/builder.go) | 修改链式构建器的 JOIN 方法 |
|
|
||||||
@ -20,9 +20,21 @@ func NewIdentifierProcessor(dialect Dialect, prefix string) *IdentifierProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 系统数据库列表,这些数据库不添加前缀
|
||||||
|
var systemDatabases = map[string]bool{
|
||||||
|
"INFORMATION_SCHEMA": true,
|
||||||
|
"information_schema": true,
|
||||||
|
"mysql": true,
|
||||||
|
"performance_schema": true,
|
||||||
|
"sys": true,
|
||||||
|
"pg_catalog": true,
|
||||||
|
"pg_toast": true,
|
||||||
|
}
|
||||||
|
|
||||||
// ProcessTableName 处理表名(添加前缀+引号)
|
// ProcessTableName 处理表名(添加前缀+引号)
|
||||||
// 输入: "order" 或 "`order`" 或 "\"order\""
|
// 输入: "order" 或 "`order`" 或 "\"order\"" 或 "INFORMATION_SCHEMA.TABLES"
|
||||||
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
||||||
|
// 对于 database.table 格式,会分别处理,系统数据库不添加前缀
|
||||||
func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
||||||
// 去除已有的引号
|
// 去除已有的引号
|
||||||
name = p.stripQuotes(name)
|
name = p.stripQuotes(name)
|
||||||
@ -33,7 +45,23 @@ func (p *IdentifierProcessor) ProcessTableName(name string) string {
|
|||||||
parts := strings.SplitN(name, " ", 2)
|
parts := strings.SplitN(name, " ", 2)
|
||||||
tableName := p.stripQuotes(parts[0])
|
tableName := p.stripQuotes(parts[0])
|
||||||
alias := parts[1]
|
alias := parts[1]
|
||||||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + " " + alias
|
// 递归处理表名部分(可能包含点号)
|
||||||
|
return p.ProcessTableName(tableName) + " " + alias
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含点号(database.table 格式)
|
||||||
|
if strings.Contains(name, ".") {
|
||||||
|
parts := p.splitTableColumn(name)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
dbName := p.stripQuotes(parts[0])
|
||||||
|
tableName := p.stripQuotes(parts[1])
|
||||||
|
// 系统数据库不添加前缀
|
||||||
|
if systemDatabases[dbName] {
|
||||||
|
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(tableName)
|
||||||
|
}
|
||||||
|
// 非系统数据库,只给表名添加前缀
|
||||||
|
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(p.prefix+tableName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加前缀和引号
|
// 添加前缀和引号
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user