Skip to content

Phase 3: 数据库模块 ​

目标:实现 Aifei Go 的数据库访问层,包含 Db、Row、Dao、Page、Dialect、Enjoy SQL 模板引擎、Generator 代码生成器

1. 模块结构 ​

db/                       # 数据库访问库(零外部依赖,完全不知道 generator 的存在)
├── db.go                # Db 入口 (快捷方法)
├── dao.go               # Dao 链式调用对象
├── row.go               # Row 数据行 (Active Record)
├── page.go              # Page 分页结果
├── batch.go             # Batch 批量操作
├── dialect.go           # Dialect 接口 + MySQL/PostgreSQL/SQLite 实现
├── config.go            # 数据库配置 (含 SqlKit 集成)
├── transaction.go       # 事务管理
├── type_converter.go    # 类型转换器
├── table.go             # Table 运行时元数据
│
└── sql/                 # Enjoy SQL 模板引擎 (已实现)
    ├── kit.go               # SqlKit — 包装 Enjoy 引擎
    ├── para.go              # SqlPara — SQL + 参数
    ├── directive.go         # Directive 接口
    ├── para_directive.go    # #para / #p 指令
    ├── where_directive.go   # #where 指令
    ├── and_directive.go     # #and 指令
    ├── orderby_directive.go # #orderBy 指令
    ├── condition.go         # SqlCondition 类型
    ├── keys.go              # SQL 模板内部 key 常量
    └── operator.go          # SqlOperator 枚举 (18 种操作符)

generator/                # 代码生成器独立模块(import db + enjoy)
    ├── generator.go         # Generator — 主入口
    ├── meta_reader.go       # MetaReader — 读取数据库元数据
    ├── meta_dialect.go      # MetaDialect — generator 专用方言接口
    ├── type_mapping.go      # TypeMapping — SQL 类型 → Go 类型
    ├── field_to_attr.go     # FieldToAttr — 字段名 → 属性名
    ├── go_keyword.go        # GoKeyword — Go 保留字检查
    ├── types.go             # TableInfo / FieldInfo 数据结构
    ├── names.go             # buildNames — 标识符参数化(默认裸名 / Qualified 表限定名)
    ├── template_util.go     # TemplateUtil — Enjoy 模板辅助方法
    ├── base_generator.go    # BaseGenerator — 生成 base.go (覆盖写入,含 FromRow/FromRows 桥)
    ├── model_generator.go   # ModelGenerator — 生成 model 文件 (存在则跳过)
    ├── dao_generator.go     # DaoGenerator — 生成 dao.go (存在则跳过;typed 批量 IN + XxxPage 分页)
    ├── service_generator.go # ServiceGenerator — 生成 service.go (存在则跳过;Template 可自定义)
    ├── init_generator.go    # InitGenerator — 生成 init.go (覆盖写入,按包去重空白导入)
    └── templates/           # Enjoy 模板文件 (.af)
        ├── _base.af             # 生成 BaseXxx + Table + getter/setter
        ├── _model.af            # 生成 Xxx struct
        ├── _dao.af              # 生成 Dao + 查询函数
        ├── _service.af          # 生成 Service + HTTP 路由
        └── _init.af             # 生成 init.go(各包空白导入,触发 init() 自注册)

依赖方向(无循环):


2. Enjoy SQL 模板引擎(db/sql/,已实现) ​

概述 ​

Enjoy SQL 模板引擎是 Aifei db 的核心亮点之一。它利用 Enjoy 模板引擎的自定义指令(Directive)机制,在 SQL 中嵌入动态逻辑(条件 where、动态排序、参数占位等),解决传统 SQL 拼接的痛点。

架构 ​

核心 API ​

go
// 创建 SqlKit
sk := sql.NewSqlKit("main")

// 获取 SqlPara — 直接传入 SQL 模板字符串
sp := sk.GetSqlPara(
    "select * from user #where() #and(age > #para(age)) #and(name like #para(name)) #orderBy(orderBy)",
    map[string]interface{}{"age": 18, "name": "%james%", "orderBy": "id desc"},
)
// sp.Sql = "select * from user where age > ? and name like ? order by id desc"
// sp.Paras = [18, "%james%"]

// 缓存 SQL 模板 — 通过 ID 引用
sk.AddSql("user.list", "select * from user #where() #and(status = #para(status))")
sp := sk.GetSqlParaByID("user.list", map[string]interface{}{"status": 1})

// 位置参数方式
sp := sk.GetSqlParaWithArgs("select * from user where id = #para(0)", 123)

与 Db/Dao 集成 ​

SqlKit 通过 Config 绑定到每个数据库连接:

go
// Db 层面的快捷方法
db.Sql("select * from user #where() #and(age > #para(age))", map[string]interface{}{"age": 18}).Find()
db.SqlWithArgs("select * from user where id = #para(0)", 123).FindFirst()
db.SqlById("user.list", map[string]interface{}{"status": 1}).Find()
db.AddSql("user.list", "select * from user #where() #and(status = #para(status))")

3. Db 入口(已实现) ​

go
package db

// 初始化
func Init(driverName, dsn string, opts ...ConfigOption) error
func InitWithID(configID, driverName, dsn string, opts ...ConfigOption) error

// 获取配置
func GetConfig(id ...string) *Config
func ResetConfigs()

// 获取 Dao
func Use() *Dao
func UseWithID(configID string) *Dao

// SQL 入口 (链式查询起点)
func SQL(query string, args ...interface{}) *Dao                    // 原始 SQL
func Sql(sqlStr string, data map[string]interface{}) *Dao           // Enjoy SQL 模板
func SqlWithArgs(sqlStr string, args ...interface{}) *Dao           // Enjoy SQL 模板 + 位置参数
func SqlById(sqlID string, data map[string]interface{}) *Dao        // 缓存 SQL 模板
func SqlByIdWithArgs(sqlID string, args ...interface{}) *Dao        // 缓存 SQL 模板 + 位置参数
func AddSql(sqlID, sql string)                                      // 缓存 SQL 模板
func AddSqlWithID(configID, sqlID, sql string)                      // 指定配置缓存
func Select(fields string) *Dao

// 快捷 CRUD
func Insert(row *Row) (*Row, error)
func InsertOrUpdate(row *Row) (*Row, error)
func Update(row *Row) (bool, error)
func Delete(row *Row) (bool, error)
func DeleteByID(table string, id interface{}) (bool, error)
func DeleteByIDWithPK(table, pk string, id interface{}) (bool, error)
func DeleteBy(table, whereOrField string, args ...interface{}) (int64, error)
func FindByID(table string, id interface{}) (*Row, error)
func FindByIDWithPK(table, pk string, id interface{}) (*Row, error)
func FindBy(table, whereOrField string, args ...interface{}) ([]*Row, error)
func FindFirstBy(table, whereOrField string, args ...interface{}) (*Row, error)
func FindIn(table, field string, values ...interface{}) ([]*Row, error)
func Count(table string) (int64, error)
func CountBy(table, whereOrField string, args ...interface{}) (int64, error)

// 事务 / 批量
func Transaction(fn func() error) error
func TransactionWithID(configID string, fn func() error) error
func TxBegin(configID ...string) (*sql.Tx, error)
func NewBatch() *Batch

// Row 工厂
func NewRow(table string) *Row
func NewRowWithPK(table, pk string) *Row
func NewRowWithCompositePK(table, pk1, pk2 string) *Row

// Table 元数据
func RegisterTable(t *Table)
func Tables() []*Table

4. Dao(已实现) ​

go
type Dao struct {
    config    *Config
    sqlStr    string
    sqlArgs   []interface{}
    sqlPara   *sql.SqlPara
    selFields string
    fromTable string
}

// 链式入口
func (d *Dao) SQL(query string, args ...interface{}) *Dao
func (d *Dao) Sql(sqlStr string, data map[string]interface{}) *Dao
func (d *Dao) SqlWithArgs(sqlStr string, args ...interface{}) *Dao
func (d *Dao) SqlById(sqlID string, data map[string]interface{}) *Dao
func (d *Dao) SqlByIdWithArgs(sqlID string, args ...interface{}) *Dao
func (d *Dao) Select(fields string) *Dao
func (d *Dao) SqlPara(sp *SqlPara) *Dao

// 链式终止 (查询)
func (d *Dao) Find() ([]*Row, error)
func (d *Dao) FindFirst() (*Row, error)
func (d *Dao) FindAll() ([]*Row, error)
func (d *Dao) Paginate(pageNum, pageSize int) (*Page, error)

// 链式终止 (更新)
func (d *Dao) Update() (int64, error)
func (d *Dao) InsertRow(row *Row) (*Row, error)
func (d *Dao) InsertOrUpdateRow(row *Row) (*Row, error)
func (d *Dao) UpdateRow(row *Row) (bool, error)
func (d *Dao) DeleteRow(row *Row) (bool, error)

// Table CRUD
func (d *Dao) FindByID(table string, id interface{}) (*Row, error)
func (d *Dao) FindByIDWithPK(table, pk string, id interface{}) (*Row, error)
func (d *Dao) FindBy(table, whereOrField string, args ...interface{}) ([]*Row, error)
func (d *Dao) FindFirstBy(table, whereOrField string, args ...interface{}) (*Row, error)
func (d *Dao) DeleteByID(table string, id interface{}) (bool, error)
func (d *Dao) DeleteByIDWithPK(table, pk string, id interface{}) (bool, error)
func (d *Dao) DeleteBy(table, whereOrField string, args ...interface{}) (int64, error)
func (d *Dao) Count(table string) (int64, error)
func (d *Dao) CountBy(table, whereOrField string, args ...interface{}) (int64, error)
func (d *Dao) FindIn(table, field string, values ...interface{}) ([]*Row, error)
func (d *Dao) DeleteIn(table, field string, values ...interface{}) (int64, error)

5. Row(已实现) ​

go
type Row struct {
    table       string
    primaryKeys []string
    data        map[string]interface{}
    change      map[string]struct{} // 追踪 Set 操作的字段
}

// 工厂方法 (通过 db.NewRow / db.NewRowWithPK)

// Set 系列 (追踪 change — 用于 UPDATE)
func (r *Row) Set(field string, value interface{}) *Row
func (r *Row) SetMap(data map[string]interface{}) *Row
func (r *Row) SetIfNotNull(field string, value interface{}) *Row
func (r *Row) SetIfNotBlank(field string, value interface{}) *Row

// Put 系列 (不追踪 change — 仅填充数据)
func (r *Row) Put(field string, value interface{}) *Row
func (r *Row) PutMap(data map[string]interface{}) *Row

// 字段操作
func (r *Row) Remove(fields ...string) *Row
func (r *Row) RemoveNullFields() *Row
func (r *Row) Keep(fields ...string) *Row
func (r *Row) Clear() *Row

// Change 追踪
func (r *Row) ChangeSet() map[string]interface{}
func (r *Row) ClearChange()
func (r *Row) ChangedFields() []string

// 类型安全 Getter
func (r *Row) GetStr(field string) string
func (r *Row) GetInt(field string) int
func (r *Row) GetInt64(field string) int64
func (r *Row) GetFloat64(field string) float64
func (r *Row) GetBool(field string) bool
func (r *Row) GetTime(field string) time.Time
func (r *Row) GetBytes(field string) []byte
func (r *Row) Get(field string) interface{}
// 带默认值版本: GetStr, GetInt

// 主键
func (r *Row) ID(id interface{}) *Row
func (r *Row) GetID() interface{}
func (r *Row) CompositeID(id1, id2 interface{}) *Row

// 元数据
func (r *Row) Table() string
func (r *Row) SetTable(table string)
func (r *Row) PrimaryKeys() []string
func (r *Row) SetPrimaryKeys(pks []string)
func (r *Row) Has(field string) bool
func (r *Row) Size() int
func (r *Row) FieldNames() []string
func (r *Row) FieldValues() []interface{}
func (r *Row) ForEach(fn func(field string, value interface{}))

// JSON
func (r *Row) MarshalJSON() ([]byte, error)
func (r *Row) UnmarshalJSON(data []byte) error

// Active Record
func (r *Row) Insert() (*Row, error)
func (r *Row) InsertOrUpdate() (*Row, error)
func (r *Row) Update() (bool, error)
func (r *Row) Delete() (bool, error)

6. Page / Dialect / Batch / Transaction(已实现) ​

Page ​

go
type Page struct {
    PageNum    int    `json:"pageNum"`
    PageSize   int    `json:"pageSize"`
    TotalRows  int64  `json:"totalRows"`
    TotalPages int    `json:"totalPages"`
    Rows       []*Row `json:"rows"`
}

func NewPage(pageNum, pageSize int, totalRows int64, rows []*Row) *Page
func (p *Page) IsFirstPage() bool
func (p *Page) IsLastPage() bool
func (p *Page) HasPreviousPage() bool
func (p *Page) HasNextPage() bool

Dialect ​

go
type Dialect interface {
    Name() string
    DefaultPrimaryKeys() []string
    ForCountSubquery(query string) string
    ForPaginate(query string, pageNum, pageSize int) string
    ForFindByID(table string, primaryKeys []string) string
    ForDeleteByID(table string, primaryKeys []string) string
    ForInsert(table string, fields []string) string
    ForUpdate(table string, fields []string, primaryKeys []string) string
    ForInsertOrUpdate(table string, fields []string, primaryKeys []string) string
}

// 内置实现
type MySQLDialect struct{}
type PostgresDialect struct{}
type SQLiteDialect struct{}

func NewDialect(driverName string) Dialect

Batch / Transaction ​

go
// Batch
type Batch struct { ... }
func NewBatch() *Batch
func (b *Batch) Insert(rows []*Row) (*BatchResult, error)
func (b *Batch) InsertWithTable(table string, rows []*Row) (*BatchResult, error)
func (b *Batch) Update(rows []*Row) (*BatchResult, error)
func (b *Batch) UpdateWithTable(table string, rows []*Row) (*BatchResult, error)
func (b *Batch) Execute(sql string, argsList [][]interface{}) (*BatchResult, error)
func (b *Batch) ExecuteSQLs(sqls []string) (*BatchResult, error)

type BatchResult struct {
    RowsAffected int64
    Error        error
}

// Transaction
func Transaction(fn func() error) error
func TransactionWithID(configID string, fn func() error) error
func TxBegin(configID ...string) (*sql.Tx, error)

7. Generator 代码生成器(已实现) ​

7.1 设计动机 ​

在 Java Aifei 中,Generator 是 aifei-db 的两个核心组件之一。它通过读取数据库元数据自动生成类型安全的 CRUD 代码:

  • 消除手写样板代码:每个数据表需要手写大量 getter/setter/CRUD 方法,Generator 自动生成类型安全的代码
  • 编译期类型检查:生成的 GetName() string 远比 GetStr("name") 安全
  • Schema 同步:数据库字段变更后重新生成 base.go 即可,model/dao/service 因已存在而不会被覆盖
  • Short Setter:user.New().Name_("james").Age_(28) 链式调用风格

7.2 生成策略:默认每表一包,Qualified 多表一包(包 = 领域) ​

Java 按层级分 base/、model/、dao/ 三个包。Go 默认采用每表一个包策略:

myapp/db/
├── init.go                # 每次覆盖 — 各包空白导入,触发 init() 自注册(多包去重)
│
├── user/                  # package user(默认裸名:Table / New / Dao / FindById)
│   ├── base.go            # 每次覆盖 — BaseUser + Table + getter/setter + 实例 CRUD + FromRow/FromRows 桥
│   ├── model.go           # 存在则跳过 — User struct(用户扩展)
│   ├── dao.go             # 存在则跳过 — Dao + FindById/FindByIds/FindIn/分页等查询方法
│   └── service.go         # 存在则跳过 — Service + HTTP 方法路由
│
├── order/                 # package order
│   ├── base.go
│   ├── model.go
│   ├── dao.go
│   └── service.go

开启 Generator.Qualified = true(配合 PkgNameFunc 把多张表映射进同一包)后,同一包可以放多张表(包 = 领域):包级标识符全部带表名(TableUser / NewUserDao / UserFindById),文件按表命名(base_user.go 等),互不冲突:

myapp/db/flow/               # 两表一包:sys_flow_task + sys_flow_log
├── base_flow_task.go        # TableFlowTask / NewFlowTask / FlowTaskFindById ...
├── base_flow_log.go         # TableFlowLog / NewFlowLog / FlowLogFindById ...
├── dao_flow_task.go
└── ...

**同一包内互引无需 import,彻底消除循环依赖的风险。**跨包调用走「向下单向」:service → 他包 dao/model 是数据依赖天然无环(demo 的 flow service 组合 user 包模型即此模式);service ↔ 他包 service 才需要窄接口 / dami 事件 / 合并包三阀门(详见 generator 指南 §7.2)。

Java 层级方案Go 默认(每表一包)Go Qualified(多表一包)
base.FindUserById(id)user.FindById(id)user.UserFindById(id)
model.Useruser.Useruser.User(结构体名本就含表名)
dao.NewUserDao()user.NewDao()user.NewUserDao()

7.3 架构 ​

Generator 作为独立模块 generator/,与 db/ 完全解耦。它 import db 但 db 不知道它的存在。

MetaDialect 接口(generator 内部定义,不污染 db.Dialect):

go
package generator

// MetaDialect 是 generator 专用的方言接口,在 db.Dialect 基础上增加元数据查询方法。
type MetaDialect interface {
    db.Dialect
    QueryTableNames(db *sql.DB) ([]string, error)
    QueryTableInfo(table string) string
}

7.4 数据结构 ​

go
type FieldInfo struct {
    Name            string // 数据库字段名 (snake_case)
    GoType          string // Go 类型,如 "int", "string", "time.Time"
    AttrName        string // Go 导出字段名 (CamelCase)
    Remarks         string // 字段注释
    IsAutoIncrement bool   // 是否自增
}

type TableInfo struct {
    Name       string            // 表名
    PrimaryKey []string          // 主键列名
    Remarks    string            // 表注释
    IsView     bool              // 是否为视图
    Fields     []*FieldInfo      // 字段列表
    PkgName    string            // 包名
    StructName string            // 结构体名
    BaseName   string            // 基础结构体名
    Names      map[string]string // 标识符参数化(names.go:tableVar/newFn/findByIdFn/文件名等,见 §7.2)
}

7.5 Generator — 主入口 ​

go
package generator

type Generator struct {
    pool       *sql.DB
    dialect    MetaDialect
    outputDir  string
    importRoot string

    // 可定制字段
    TablePrefix    string              // 表名前缀,命名前剥离(如 "sys_")
    Qualified      bool                // 多表一包:包级标识符带表名、文件按表命名
    Force          bool                // 强制覆盖 model/dao/service(默认已存在则跳过)
    PkgNameFunc    func(string) string // 表名 → 包名(多表映射同一包即「包=领域」)
    StructNameFunc func(string) string // 表名 → 结构体名
    BaseNameFunc   func(string) string // 结构体名 → base 结构体名

    metaReader       *MetaReader
    baseGenerator    *BaseGenerator
    modelGenerator   *ModelGenerator
    daoGenerator     *DaoGenerator
    initGenerator    *InitGenerator
    serviceGenerator *ServiceGenerator
}

func New(pool *sql.DB, dialect MetaDialect, outputDir, importRoot string) *Generator

func (g *Generator) ConfigMetaReader(fn func(*MetaReader)) *Generator
func (g *Generator) ConfigBaseGenerator(fn func(*BaseGenerator)) *Generator
func (g *Generator) ConfigModelGenerator(fn func(*ModelGenerator)) *Generator
func (g *Generator) ConfigDaoGenerator(fn func(*DaoGenerator)) *Generator
func (g *Generator) ConfigServiceGenerator(fn func(*ServiceGenerator)) *Generator

func (g *Generator) Generate() error

Generate() 内部校验:多张表被映射进同一包而未开 Qualified 时直接报错并列出冲突表名(宁可失败,不产出撞名代码)。

7.6 使用示例 ​

go
import (
    "github.com/crazy-airhead/aifei-go/db"
    "github.com/crazy-airhead/aifei-go/tools/generator"
)

func main() {
    db.Init("sqlite", "app.db")
    pool, _ := db.GetConfig().Pool()

    dialect := generator.NewMetaDialect(db.GetConfig().GetDialect())
    gen := generator.New(pool, dialect, "./myapp/db", "myapp/db")

    // 可选:按领域多表一包(如 flow_task/flow_log → flow 包)
    // gen.TablePrefix = "sys_"
    // gen.Qualified = true
    // gen.PkgNameFunc = func(tableName string) string { ... }

    gen.ConfigMetaReader(func(mr *generator.MetaReader) {
        mr.AddBlacklist("migrations", "schema_version")
    })

    gen.Generate() // 默认每个表一个包:user/、order/、product/
}

7.7 Go vs Java 生成差异对比 ​

方面Java 版Go 版
包结构base/ model/ dao/ 三层默认每表一包;Qualified 多表一包(包=领域)+ 汇总 init.go 空白导入
模型注册ModelSet — LinkedHashSet<Class> 用于 DI 扫描每包 init() 调 db.RegisterTable(Table);init.go 空白导入触发,db.Tables() 遍历
继承方式BaseUser<M> extends AifeiRow<M> + CRTPBaseUser 内嵌 *db.Row
模型类User extends BaseUser<User>同包内 User 嵌入 *BaseUser
查询方法User.findById(123)user.FindById(123)(Qualified:user.UserFindById(123))— 包名即命名空间
Dao 实例new UserDao()user.NewDao()(Qualified:user.NewUserDao())— 无冗余命名
ServiceController 层service.go — 同包内 HTTP 方法路由(模板可经 Template 字段整体替换)
模板引擎Enjoy 模板 (.af)Enjoy 模板 (.af) — 相同引擎,渲染结果统一过 gofmt
Short Setteruser.name("james")user.New().Name_("james")(Qualified:user.NewUser()...)

7.8 Enjoy 代码生成模板 ​

使用 Enjoy 模板引擎生成代码(与 Java 版一致),通过 //go:embed 嵌入:

  • _base.af — 生成 base.go:BaseXxx 结构体、Table 变量、类型安全 getter/setter/short setter、CRUD 实例方法、导出的 FromRow/FromRows 桥(裸 *db.Row → 类型化模型)。每次覆盖写入。
  • _model.af — 生成 model 文件:Xxx 结构体嵌入 *BaseXxx。存在则跳过(Force 覆盖)。
  • _dao.af — 生成 dao.go:typed Dao 结构体(两段式 setup → terminal,返回 []*Xxx)、包级查询函数(FindById、FindByIds/DeleteByIds 批量 IN、FindIn、FindBy、DeleteById、Count 等)、typed 分页 XxxPage(Rows []*Xxx)。存在则跳过(Force 覆盖)。
  • _service.af — 生成 service.go:Service 结构体、init() 自注册、HTTP 方法路由(List、Paginate、Create、GetById、UpdateById、DeleteById)。存在则跳过(Force 覆盖);ServiceGenerator.Template 可注入应用自有模板(同一数据 map)。
  • _init.af — 生成 init.go:各包空白导入(多表一包时按包去重),触发各包 init() 自注册。每次覆盖写入。

模板中的包级标识符全部参数化(names.go 注入 Table / NewDao / FindById 或 Qualified 作用域的 TableUser / NewUserDao / UserFindById),一套模板服务两种包结构。


8. 完整使用示例对比 ​

原生 Row API ​

go
// 插入
row := db.NewRow("user").Set("name", "james").Set("age", 18)
db.Insert(row)

// 查询
rows, _ := db.FindBy("user", "age > ?", 18)
name := rows[0].GetStr("name") // 无编译期类型检查

// 更新
db.Update(db.NewRow("user").ID(123).Set("name", "james zhan"))

Generator 生成的类型安全 API(每表一包) ​

go
import "myapp/db/user"

// 插入 — 类型安全 + Short Setter
u := user.New().Name_("james").Age_(28)
u.Insert()

// 查询 — 包名即命名空间
u, _ := user.FindById(123)
fmt.Println(u.Name()) // 类型安全,IDE 自动补全

list, _ := user.FindBy("age > ?", 18)

// 更新
u, _ := user.FindById(123)
u.SetName("james zhan").Update()

// 删除
user.DeleteById(123)

Enjoy SQL 模板 + 生成的 Dao ​

go
import "myapp/db/user"

// 注册 SQL 模板
db.AddSql("user.listByAge", `
    select * from user
    #where()
    #and(age > #para(minAge))
    #and(status = #para(status))
    #orderBy(orderBy)
`)

// 包级函数 — 简单查询
list, _ := user.FindBy("age > ?", 18)

// Dao 链式调用 — 复杂查询
dao := user.NewDao()
page, _ := dao.SqlById("user.listByAge", map[string]interface{}{
    "minAge":  18,
    "status":  1,
    "orderBy": "id desc",
}).Paginate(1, 10)

基于 Apache-2.0 许可发布