OOro

核心概念

理解 Oro 的入口、模型、字段名、Map、Null、同步和驱动边界。

Oro 的目标是把 ORM 的概念压到少数几个清晰入口上。使用者不需要记一堆历史 API,也不需要运行代码生成器。

三个查询入口

db.Use[Product]()      // 模型查询
db.Table("products")   // 裸表查询
db.Raw("select ...")   // 原生 SQL
入口 使用场景 字段命名 返回值
Use[T]() 业务模型查询 Go 字段名,如 Price *T / []*T
Table(name) 直接查表、封装底层能力 数据库列名,如 price oro.Map / []oro.Map
Raw(sql) 原生 SQL 逃生口 手写 SQL oro.Map,可 MapTo[T]()

模型查询适合日常业务。裸表查询适合后台工具、报表、动态表名、低层封装。Raw 只用于 ORM DSL 不该覆盖的场景。

模型和表

模型是带 Define 方法的 Go 结构体:

type Product struct {
    oro.Model
    Code  string
    Price uint
}

func (Product) Define(s *oro.SchemaBuilder) {
    s.Table("products")
    s.Field("Code").String().Unique()
    s.Field("Price").Uint().Default(0)
}

Define 是数据库结构来源。Oro 不使用 tag 描述字段类型,这样字段定义可以带方法、索引、默认值、连接和分片配置,不会把复杂配置塞进字符串 tag。

Go 字段名和数据库列名

模型查询使用 Go 字段名:

db.Use[Product]().Where("Price", ">=", 100)
db.Use[Product]().OrderBy("ID")

裸表查询使用数据库列名:

db.Table("products").Where("price", ">=", 100)
db.Table("products").OrderBy("id")

这个规则避免了 ORM 在不同入口里偷偷转换字段名。你看到的是哪一层,就写哪一层的名字。

oro.Map

oro.Map 是统一的数据容器:

oro.Map{"Price": 100}
oro.Map{"price": 100}

它用于:

  • 模型更新;
  • 裸表写入;
  • Raw / Table 查询结果;
  • 扩展状态;
  • 多对多中间表字段。

模型查询里的 oro.Map 使用 Go 字段名;裸表查询里的 oro.Map 使用数据库列名。

oro.Null[T]

数据库 NULL 不等于 Go 零值。Oro 使用 oro.Null[T] 表达 nullable 字段:

type Product struct {
    oro.Model
    Stock oro.Null[int]
}

product.Stock = oro.NullOf(10)
product.Stock = oro.NullZero[int]()

这样业务代码不用到处处理 *int*string 的取值问题,也能明确区分“有值 0”和“数据库 NULL”。

自动同步

db.Register(Product{}, User{})
db.Sync(ctx)

自动同步只做安全变更:建表、加字段、建索引、维护结构快照。删除字段、危险类型变更、收紧 nullable 约束不会静默执行。

Oro 不要求每次早期结构调整都写迁移文件,但也不会冒险替你做破坏性变更。结构来源始终是 Go 模型定义。

驱动边界

官方驱动包是 database/sql 包装器和方言适配器,不主动注册具体数据库实现。

import (
    "github.com/duxweb/oro/driver/sqlite"
    _ "modernc.org/sqlite"
)

如果你想使用 mattn/go-sqlite3

import _ "github.com/mattn/go-sqlite3"

db, err := oro.Open(oro.Config{
    Connections: map[string]oro.ConnectionConfig{
        "default": {Driver: sqlite.Open("app.db", sqlite.DriverName("sqlite3"))},
    },
})

高级场景可以直接传入已有连接:

sqlDB, _ := sql.Open("sqlite3", "app.db")
driver := sqlite.Wrap(sqlDB, sqlite.OwnsDB(true))

返回规则

查询不到不是错误:

方法 查不到时
First(ctx) nil, nil
Find(ctx, id) nil, nil
Get(ctx) 空切片
Count(ctx) 0, nil

错误只表示 SQL、连接、约束、事务或扫描出现异常。

关系不是结构体字段

Oro 不要求模型里写 Cover *Image 这类字段。关系是方法:

func (article Article) Cover() oro.Relation {
    return oro.HasOne(article, "Cover", "Image").
        ForeignKey("ArticleID").
        ReferenceKey("ID")
}

读取时也通过方法:

cover, err := article.Cover().One[Image]()
comments, err := article.Comments().Many[Comment]()

这种设计能减少 Go 包循环依赖,也让预加载状态更明确。

编辑此页