| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- package DB
- import (
- "errors"
- "fmt"
- "git.ycl.pub/yl32940/TsDB2/DB/core/page"
- )
- // ============================================================================
- // 表基类: 会话绑定 + 头部增删改查
- // ============================================================================
- //
- // 三张公开面孔:
- //
- // BaseModel 主键表基类 = PrimaryModel (别名) 内嵌 → _id 列 + B+ 树主键索引
- // TimeSeriesModel 时序表基类 内嵌 → _ts 列 + 时序索引
- //
- // 表**必须**内嵌其中之一: 它既是"这是一张本框架的表"的标记 (ParseModel / UseTable
- // 对没有基类的结构体直接报 ErrNotModel), 也让对象的"头部"有了增删改查:
- //
- // type User struct {
- // DB.BaseModel
- // Name string `vrtx:"maxlen:32"`
- // }
- //
- // tbl, _ := DB.UseTable[User](db)
- // u := tbl.New() // 已绑定
- // u.Name = "a"
- // u.Insert() // 增 (自增主键自动回写 u.ID)
- // u.Name = "b"
- // u.Update() // 改 (按 u.ID)
- // u.Reload() // 查 (按 u.ID 重新读进 u)
- // u.Delete() // 删 (按 u.ID)
- //
- // 为什么要把 sess / self 存在基类里 —— Go 的嵌入不是继承:
- //
- // 方法被提升到外层结构体之后, 接收者仍然是那个被嵌入的基类值, 它没有任何办法回头
- // 拿到"外层对象"。而增删改查必须拿到外层对象 (要序列化整行、要把自增主键写回字段),
- // 所以基类只能自己持有它。这两个字段由 Table[T] 在绑定时写入 —— 只有那一层知道 T。
- // modelCore 三个公开基类的共同底座, 持有"这个对象属于哪张表"。
- //
- // 单独拆成一个非导出结构体, 而不是直接塞进 BaseModel:
- // - 时序表基类要用同一套增删改查, 但它不该被塞进一个根本用不上的 _id 字段;
- // - 增删改查只保留一份实现, 而不是每个基类各写一遍 (也就不会有哪一份忘了改)。
- type modelCore struct {
- sess session // 所属表 (Table[T] 的类型擦除视图); nil = 未绑定
- self any // 外层对象的指针 (*T), 由 Table 写入
- pkRowID uint64 // 引擎行号 (pkID); 0 = 未知。对用户隐藏, 只用来精确定位"自己那一行"
- }
- // session 是 Table[T] 的类型擦除视图: 模型基类只认这一组操作, 不认识 T。
- //
- // 每个方法内部都会把参数断言回 *T。断言失败只可能是"把 A 表的对象交给了 B 表",
- // 所以返回明确错误, 而不是 panic。
- type session interface {
- insert(obj any) error
- update(obj any) error
- pkOf(obj any) (uint64, error)
- loadInto(pk uint64, obj any) error
- reload(obj any) error
- deleteByPK(pk uint64) error
- deleteByRowID(pkID uint64) error
- tableName() string
- }
- // modelBinder 由内嵌的表基类提供, 让 Table 把"这个对象属于哪张表"写回基类,
- // 也让它把"这一行在引擎里的行号"带回对象。
- //
- // 走接口断言而不是反射: 绑定是每条被读写的记录都要过一遍的路径, 反射在这里是白付的。
- // 由于 UseTable 已经强制要求内嵌基类, 这里的断言必然成功 —— 保留失败分支只是为了让
- // Table[T] 的绑定路径不依赖"上游一定校验过"这个假设 (少一处只能靠推理保证的不变量)。
- type modelBinder interface {
- bindTo(s session, self any)
- rowID() uint64
- setRowID(pkID uint64)
- }
- // rowID 返回本对象的引擎行号 (pkID); 0 表示未知。
- //
- // 行号对用户完全隐藏 —— 它是引擎给每一行发的自增内部句柄 (类似 SQLite 的 rowid),
- // 不是表结构里的一列, 也不参与查询语义。它存在的唯一理由是"同一个时间戳下可以有多行":
- // 时间戳只说得清"落在哪个分段", 只有行号能指认"是分段里的哪一行"。没有它, 同刻的
- // 第二行就没法被读 / 改 / 删精确定位 (按时间键只会碰到第一行)。
- //
- // 由 Table[T] 在 Insert / Find / Select 之后写入本对象。
- func (c *modelCore) rowID() uint64 { return c.pkRowID }
- // setRowID 由 Table[T] 写入本对象的引擎行号。
- func (c *modelCore) setRowID(pkID uint64) { c.pkRowID = pkID }
- // rowIDOf / setRowIDOf 是包内工具: 对可能没有内嵌基类的对象安全地读写行号。
- func rowIDOf(obj any) uint64 {
- if b, ok := obj.(modelBinder); ok {
- return b.rowID()
- }
- return 0
- }
- func setRowIDOf(obj any, pkID uint64) {
- if b, ok := obj.(modelBinder); ok && pkID != 0 {
- b.setRowID(pkID)
- }
- }
- // bindTo 由 Table 调用: 写入会话与外层对象指针。
- func (c *modelCore) bindTo(s session, self any) {
- c.sess = s
- c.self = self
- }
- // Bound 报告本对象是否已绑定到表。
- func (c *modelCore) Bound() bool { return c != nil && c.sess != nil }
- // ready 检查绑定状态。未绑定时给出"接下来该做什么"的错误, 而不是一句 nil 崩溃。
- func (c *modelCore) ready(op string) error {
- if !c.Bound() {
- return fmt.Errorf("%w: 不能调用 %s (先用 Table.New 创建, 或 Table.Bind 绑定, "+
- "或先经过一次 Insert / Find)", ErrModelNotBound, op)
- }
- return nil
- }
- // ===== 头部增删改查 =====
- // Insert 插入本对象 (增)。主键字段为 0 时向引擎要一个自增号并回写本对象;
- // 显式给了主键就原样写入 (重复主键报 core.ErrDuplicatePK)。
- func (c *modelCore) Insert() error {
- if err := c.ready("Insert"); err != nil {
- return err
- }
- return c.sess.insert(c.self)
- }
- // Update 按本对象自己的主键更新 (改)。
- func (c *modelCore) Update() error {
- if err := c.ready("Update"); err != nil {
- return err
- }
- return c.sess.update(c.self)
- }
- // Delete 删除本对象所在的那一行 (删)。
- //
- // 优先按引擎行号删 (对象是从表里读出来的 / 插入过, 行号已知), 这样同一时间戳下
- // 有多行时删的确实是"自己那一行"; 行号未知时 (外部手工构造的对象) 才退回按可见
- // 主键 (_id / _ts) 删。
- func (c *modelCore) Delete() error {
- if err := c.ready("Delete"); err != nil {
- return err
- }
- if id := c.rowID(); id != 0 {
- return c.sess.deleteByRowID(id)
- }
- pk, err := c.PKErr()
- if err != nil {
- return err
- }
- if pk == 0 {
- return fmt.Errorf("%w: Delete 需要一个非 0 主键来定位行", ErrZeroPrimaryKey)
- }
- return c.sess.deleteByPK(pk)
- }
- // Save 没见过这一行就插入, 见过就更新 —— 不想区分"新对象 / 已有对象"时用它。
- //
- // "见过"以引擎行号为准 (插入过或从表里读出来的对象都有行号); 行号未知时退回首部的
- // 老约定: 可见主键为 0 当新对象插入。
- func (c *modelCore) Save() error {
- if err := c.ready("Save"); err != nil {
- return err
- }
- if c.rowID() != 0 {
- return c.sess.update(c.self)
- }
- pk, err := c.PKErr()
- if err != nil {
- return err
- }
- if pk == 0 {
- return c.sess.insert(c.self)
- }
- return c.sess.update(c.self)
- }
- // Load 按主键把整行读进本对象 (查)。
- //
- // 之所以是"填自己"而不是"返回新对象": 头部方法没有类型参数, 拿不到 T, 也就没法
- // 返回 *T。要一个新建的、类型安全的对象请用 Table[T].Find。
- func (c *modelCore) Load(pk uint64) error {
- if err := c.ready("Load"); err != nil {
- return err
- }
- return c.sess.loadInto(pk, c.self)
- }
- // Reload 重新读一遍本对象那一行 (行被别处改过时用它刷新)。
- //
- // 走的是 Table.reload: 行号已知就按行号读 —— 时序表里"按时间键读"只会读到该时刻的
- // 第一行, 拿到同刻多行中的第二行时那样会读串行。
- func (c *modelCore) Reload() error {
- if err := c.ready("Reload"); err != nil {
- return err
- }
- return c.sess.reload(c.self)
- }
- // PK 返回本对象当前的主键值; 取不到 (未绑定 / 非法) 时为 0。
- func (c *modelCore) PK() uint64 {
- pk, _ := c.PKErr()
- return pk
- }
- // PKErr 同 PK, 但把"未绑定"和"主键非法 (负数等)"的错误也交出来。
- func (c *modelCore) PKErr() (uint64, error) {
- if err := c.ready("PK"); err != nil {
- return 0, err
- }
- return c.sess.pkOf(c.self)
- }
- // TableName 返回本对象所属的表名 (未绑定时为空串)。
- func (c *modelCore) TableName() string {
- if !c.Bound() {
- return ""
- }
- return c.sess.tableName()
- }
- // ===== 公开基类 =====
- // BaseModel 主键表基类 —— 内嵌后自动获得 _id 主键列 (B+ 树主键索引) 和头部增删改查。
- //
- // type User struct {
- // DB.BaseModel
- // Name string `vrtx:"maxlen:32,index:idx_name"`
- // Age int64 `vrtx:"index:idx_age,type:hash"`
- // Score int // 自动 int→int64
- // }
- //
- // 主键可以被 vrtx:"pk" 覆盖成自己的字段 (见 types.go 的 ParseModel 说明):
- //
- // type Ticket struct {
- // DB.BaseModel
- // No int64 `vrtx:"pk"` // 主键列变成 No, _id 不再建列
- // Name string `vrtx:"maxlen:32"`
- // }
- type BaseModel struct {
- modelCore
- ID uint64 `vrtx:"_id"`
- }
- // PrimaryModel 主键表基类, 与 BaseModel 完全等价。
- //
- // 用别名而不是再定义一个同构结构体: 主键表基类本来就只有一种物理形态
- // (B+ 树主键索引 + 默认 _id 列), 复制一份结构体只会让 ParseModel 多出一个必须
- // 等价处理的分支 —— 多一份必须同步维护的"真相"。别名既给了这个名字, 又不引入第二份定义。
- type PrimaryModel = BaseModel
- // TimeSeriesModel 时序表基类 —— 内嵌后自动获得 _ts 时序键列 (时序索引) 和头部增删改查。
- //
- // type Sensor struct {
- // DB.TimeSeriesModel
- // Value float64
- // }
- //
- // 时序键同样可以用 vrtx:"pk" 换成自己的字段, 但注意两条约束:
- // - 键列必须是 64 位整数;
- // - 键列会被排到首列 (时序索引按行内固定偏移读时间键, 见 types.go)。
- //
- // 时序键就是"时间戳"本身, 约定用纳秒级 Unix 时间戳。Insert 时若没给 (==0), ORM 会补上
- // "写入这一刻"的纳秒时间戳, 而不是行号 —— 时序键只表达时间, 行号另有其人。
- //
- // type Tick struct {
- // DB.TimeSeriesModel
- // Stamp int64 `vrtx:"pk"` // 时序键列变成 Stamp, _ts 不再建列
- // Price float64
- // }
- //
- // 时序键不是引擎行号。同一个时间戳下允许存在多行 (同一时刻的多个测点 / 多次事件) ——
- // 时间键只决定"落在哪个分段", 行号才能指认"是分段里的哪一行"。行号由引擎自动分配,
- // 随对象在 Insert / Find / Select 之后带回 (对用户隐藏), 所以同刻多行各自都能被精确
- // 地改 / 删。详见 table.go 的 Insert / Update 与 core.Table 的 *ByPK 系列。
- type TimeSeriesModel struct {
- modelCore
- TS uint64 `vrtx:"_ts"`
- }
- // ModelSpec 表基类自报物理形态 —— 相当于其它语言里的虚函数。
- //
- // 之所以是接口而不是"基类里写死判断": ParseModel 只负责递归结构体, 主键列叫什么、
- // 一级索引是哪种, 由基类自己回答; 于是再加一种基类只需要实现这三个方法。
- //
- // 方法名非导出: 只允许本包的基类实现, 用户结构体的嵌入不会意外满足它。
- type ModelSpec interface {
- indexType() page.IndexType // 一级 (行存储) 索引类型
- pkColumn() string // 默认主键列名
- pkGoField() string // 默认主键列对应的 Go 字段名
- }
- func (BaseModel) indexType() page.IndexType { return page.IndexTypePrimary }
- func (BaseModel) pkColumn() string { return pkColumnName }
- func (BaseModel) pkGoField() string { return "ID" }
- func (TimeSeriesModel) indexType() page.IndexType { return page.IndexTypeTime }
- func (TimeSeriesModel) pkColumn() string { return tsColumnName }
- func (TimeSeriesModel) pkGoField() string { return "TS" }
- // ===== 错误 =====
- var (
- // ErrNotModel 类型没有内嵌 DB.BaseModel / DB.PrimaryModel / DB.TimeSeriesModel。
- // 表基类既是"这是一张本框架的表"的标记 (ParseModel / UseTable 会因此拒绝该类型),
- // 也是对象头部增删改查的来源。
- ErrNotModel = errors.New("类型未内嵌表基类")
- // ErrModelNotBound 在未绑定到表的对象上调用头部方法。
- ErrModelNotBound = errors.New("模型未绑定到表")
- // ErrZeroPrimaryKey 头部方法需要对象自带主键来定位行, 但主键为 0。
- ErrZeroPrimaryKey = errors.New("主键为 0")
- )
|