mx 是一个轻量级、SQL 优先的 Go ORM。它主要面向 MySQL,提供表级链式查询、map CRUD、结构体映射、查询结果转换和常用结果集处理能力。
这个库不试图替代 SQL。简单查询可以用链式 API 提高效率;复杂 SQL、复杂 JOIN、窗口函数、事务和厂商特性,建议直接使用 db.Query / db.Exec 写 SQL。
- 基于
database/sql,默认使用github.com/go-sql-driver/mysql - 表级链式查询:
Where、In、MustIn、Joins、Fields、Group、Having、OrderBy、Limit、Page - map CRUD:
Create、Creates、Update、Delete、Read、Reads、CreateOrUpdate - 结构体 CRUD:
Create、Update、Delete、Save、Struct - 查询结果支持
RowsMap、RowMap、RowsMapInterface、RowsMapNull、结构体扫描 - 支持
BeforeCreate、AfterCreate、BeforeUpdate、AfterUpdate、BeforeDelete、AfterDelete、AfterFind钩子 - 自动读取 MySQL
information_schema.COLUMNS,用于字段识别、结构体映射和自动 JOIN
go get github.com/shesuyo/mxpackage main
import (
"fmt"
"log"
"time"
"github.com/shesuyo/mx"
)
func main() {
db, err := mx.NewDataBase("user:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true", mx.Config{
MaxIdleConns: 10,
MaxOpenConns: 50,
Timeout: 3 * time.Second,
})
if err != nil {
log.Fatal(err)
}
users := db.Table("user").
Fields("id", "name", "age").
Where("age > ?", 18).
OrderBy("id", true).
Limit(10).
RowsMap()
fmt.Println(users)
}开启调试 SQL:
db.Debug(true)
rows := db.Table("user").
Where("age > ?", 18).
RowsMap()也可以直接写 SQL:
rows := db.Query(
"SELECT id, name FROM user WHERE age > ? ORDER BY id DESC LIMIT ?",
18,
10,
).RowsMap()基础查询:
rows := db.Table("user").
Fields("id", "name", "age").
Where("age >= ?", 18).
WhereLike("name", "alice").
OrderBy("created_at", true).
Page(20, 1).
RowsMap()常用条件:
t := db.Table("order")
rows := t.
WhereNotEmpty("status = ?", status).
WhereTime("created_at", mx.Time{Sday: "2026-01-01", Eday: "2026-01-31"}).
In("user_id", 1, 2, 3).
RowsMap()In 和 MustIn 的区别很重要:
// ids 为空时,In 不追加任何条件,可能查出更多数据。
rows := db.Table("user").In("id", ids).RowsMap()
// ids 为空时,MustIn 直接返回空结果,不执行查询。
rows = db.Table("user").MustIn("id", ids).RowsMap()JOIN:
rows := db.Table("user").
Joins("weapon").
Fields("user.id", "user.name", "weapon.name AS weapon_name").
RowsMap()如果自动 JOIN 规则不满足,可以传入明确条件:
rows := db.Table("user").
Joins("weapon", "weapon.owner_id = user.id").
Fields("user.id", "weapon.name AS weapon_name").
RowsMap()聚合:
rows := db.Table("user").
Fields("age", "COUNT(*) AS total").
Group("age").
Having("COUNT(*) > ?", 1).
RowsMap()查看生成的 SQL:
query, args := db.Table("user").
Fields("id", "name").
Where("age > ?", 18).
Limit(10).
Parse()创建:
id, err := db.Table("user").Create(map[string]any{
"name": "alice",
"age": 20,
})批量创建:
affected, err := db.Table("user").Creates([]map[string]any{
{"name": "alice", "age": 20},
{"name": "bob", "age": 21},
})更新:
affected, err := db.Table("user").Update(map[string]any{
"id": id,
"name": "Alice",
})自增表达式:
affected, err := db.Table("counter").Update(map[string]any{
"id": 1,
"num": mx.ExprIncrNum(3),
})删除:
affected, err := db.Table("user").Delete(map[string]any{"id": id})唯一字段检查:
id, err := db.Table("user").Create(map[string]any{
"email": "alice@example.com",
"name": "alice",
}, "email")
if err == mx.ErrInsertRepeat {
// email 已存在
}创建或更新:
err := db.Table("user").CreateOrUpdate(map[string]any{
"email": "alice@example.com",
"name": "Alice",
}, "email")默认表名和字段名会按 Go 名称转换为 snake_case:
type UserProfile struct {
ID int `json:"id"`
UserID int `json:"user_id"`
Name string `json:"name"`
}
// 默认表名:user_profile
// 默认字段:id, user_id, name自定义表名:
func (UserProfile) DBName() string {
return "member_profile"
}字段标签:
type User struct {
ID int `json:"id"`
Name string `mx:"nickname"`
Password string `mx:"-"`
}mx:"column_name"指定数据库字段名mx:"-"忽略字段- 未设置
mx时,扫描路径会优先使用json标签,再回退到字段名的 snake_case
结构体 CRUD:
u := User{Name: "alice"}
id, err := db.Create(&u)
if err != nil {
return err
}
u.Name = "Alice"
err = db.Update(&u)
_, err = db.Delete(&u)保存:
rsp, err := db.Table("user").Save(&u, mx.OmitFields{"temporary_field"})Save 会根据结构体的 ID 是否为 0 自动选择创建或更新。
查询到结构体:
var users []User
err := db.Table("user").
Where("age > ?", 18).
Struct(&users)
var one User
err = db.Table("user").
WhereID(1).
Limit(1).
Struct(&one)结构体可以实现这些方法:
func (u *User) BeforeCreate() error { return nil }
func (u *User) AfterCreate() error { return nil }
func (u *User) BeforeUpdate() error { return nil }
func (u *User) AfterUpdate() error { return nil }
func (u *User) BeforeDelete() error { return nil }
func (u *User) AfterDelete() error { return nil }
func (u *User) AfterFind() error { return nil }不同调用路径对钩子错误的处理略有差异。业务逻辑依赖钩子错误时,建议补充测试锁定行为。
RowsMap / RowMap 默认把数据库值转成字符串:
row := db.Table("user").WhereID(1).RowMap()
name := row["name"]
age := row.Int("age")
rows := db.Table("user").RowsMap()
ids := rows.PluckInt("id")
byID := rows.MapIndexInt("id")
byStatus := rows.MapIndexs("status")常用方法:
row.HaveRecord()
row.NotFound()
row.Int("id")
row.Float64("amount")
row.Bool("enabled")
row.Unmarshal("json_field", &dst)
rows.First()
rows.Len()
rows.Sum("amount")
rows.Filter("status", "ok")
rows.FilterIn("id", []string{"1", "2"})
rows.Unique("user_id")
rows.SortInt("id", true)需要保留 SQL NULL 时使用:
rows := db.Table("user").RowsMapNull()需要更低分配的结果时使用:
cols, data := db.Table("user").Fields("id", "name").DoubleSlice()
_ = cols
_ = dataTripleByte 仅为兼容旧版结构体映射路径保留,不再推荐业务代码直接使用。低分配读取优先使用 DoubleSlice,结构体读取使用 Struct。
查询结果转换路径可以用专门的 benchmark 对比。默认命令只跑 stub 驱动,不依赖外部数据库:
go test -run=^$ -bench "BenchmarkQueryPath" -benchmem -count=5这组基准固定返回 128 行、8 列数据,用来比较手写 sql.RawBytes 扫描、RowMap、RowsMap、RowsMapInterface、RowsMapNull、DoubleSlice、兼容保留的 TripleByte、SQLRows.ToStruct 和 Table.Struct 的转换开销。ns/op 看单次查询路径耗时,B/op 和 allocs/op 看内存分配压力。
真实数据库对照仍然沿用集成测试开关,结果会受到网络、索引、数据量和数据库状态影响:
$env:MX_INTEGRATION='1'
$env:MX_DSN='user:password@tcp(127.0.0.1:3306)/schema?parseTime=true'
go test -run=^$ -bench "BenchmarkIntegrationQueryPath" -benchmem -count=5Table.Delete、Table.DeleteID、Table.DeleteIDs 和结构体删除始终执行物理删除。is_deleted 只被视为普通字段,mx 不会自动追加查询条件,也不会在删除时自动更新它或 deleted_at。
业务需要软删除时,应由调用方显式添加 is_deleted = 0 查询条件,并使用 Update 写入删除状态和删除时间。
Table.Update默认使用id作为条件,并且总是追加LIMIT 1Table.Delete不会自动追加LIMIT 1,会物理删除所有匹配条件的数据Table.Delete(map[string]any{})会返回ErrNoDeleteKeyIn/NotIn传入空数组时不会追加条件;空数组应返回空结果时请使用MustIn- 结构体 CRUD 和结构体扫描必须传指针,例如
db.Create(&u)、table.Struct(&users) RowsMap和大部分RowsMapInterface结果值是字符串;需要NULL语义时使用RowsMapNullGetFullSQL只适合调试日志,不是 SQL 转义函数,执行 SQL 时仍应使用参数绑定- 复杂 SQL 建议直接写
db.Query/db.Exec,不要为了使用 ORM 强行拼链式调用
NewDataBase 中保留了部分驱动前缀识别:
postgres://dm://db2://sqlserver://
当前模块只直接引入 MySQL 驱动。使用其他数据库时,应用侧需要自行引入对应 database/sql 驱动,并验证字段元数据、SQL 方言和扫描行为。
64 位操作系统。