Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

151 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mx

mx 是一个轻量级、SQL 优先的 Go ORM。它主要面向 MySQL,提供表级链式查询、map CRUD、结构体映射、查询结果转换和常用结果集处理能力。

这个库不试图替代 SQL。简单查询可以用链式 API 提高效率;复杂 SQL、复杂 JOIN、窗口函数、事务和厂商特性,建议直接使用 db.Query / db.Exec 写 SQL。

特性

  • 基于 database/sql,默认使用 github.com/go-sql-driver/mysql
  • 表级链式查询:WhereInMustInJoinsFieldsGroupHavingOrderByLimitPage
  • map CRUD:CreateCreatesUpdateDeleteReadReadsCreateOrUpdate
  • 结构体 CRUD:CreateUpdateDeleteSaveStruct
  • 查询结果支持 RowsMapRowMapRowsMapInterfaceRowsMapNull、结构体扫描
  • 支持 BeforeCreateAfterCreateBeforeUpdateAfterUpdateBeforeDeleteAfterDeleteAfterFind 钩子
  • 自动读取 MySQL information_schema.COLUMNS,用于字段识别、结构体映射和自动 JOIN

安装

go get github.com/shesuyo/mx

快速开始

package 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()

InMustIn 的区别很重要:

// 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()

map CRUD

创建:

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
_ = data

TripleByte 仅为兼容旧版结构体映射路径保留,不再推荐业务代码直接使用。低分配读取优先使用 DoubleSlice,结构体读取使用 Struct

性能基准

查询结果转换路径可以用专门的 benchmark 对比。默认命令只跑 stub 驱动,不依赖外部数据库:

go test -run=^$ -bench "BenchmarkQueryPath" -benchmem -count=5

这组基准固定返回 128 行、8 列数据,用来比较手写 sql.RawBytes 扫描、RowMapRowsMapRowsMapInterfaceRowsMapNullDoubleSlice、兼容保留的 TripleByteSQLRows.ToStructTable.Struct 的转换开销。ns/op 看单次查询路径耗时,B/opallocs/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=5

删除语义

Table.DeleteTable.DeleteIDTable.DeleteIDs 和结构体删除始终执行物理删除。is_deleted 只被视为普通字段,mx 不会自动追加查询条件,也不会在删除时自动更新它或 deleted_at

业务需要软删除时,应由调用方显式添加 is_deleted = 0 查询条件,并使用 Update 写入删除状态和删除时间。

注意事项

  • Table.Update 默认使用 id 作为条件,并且总是追加 LIMIT 1
  • Table.Delete 不会自动追加 LIMIT 1,会物理删除所有匹配条件的数据
  • Table.Delete(map[string]any{}) 会返回 ErrNoDeleteKey
  • In / NotIn 传入空数组时不会追加条件;空数组应返回空结果时请使用 MustIn
  • 结构体 CRUD 和结构体扫描必须传指针,例如 db.Create(&u)table.Struct(&users)
  • RowsMap 和大部分 RowsMapInterface 结果值是字符串;需要 NULL 语义时使用 RowsMapNull
  • GetFullSQL 只适合调试日志,不是 SQL 转义函数,执行 SQL 时仍应使用参数绑定
  • 复杂 SQL 建议直接写 db.Query / db.Exec,不要为了使用 ORM 强行拼链式调用

其他数据库驱动

NewDataBase 中保留了部分驱动前缀识别:

  • postgres://
  • dm://
  • db2://
  • sqlserver://

当前模块只直接引入 MySQL 驱动。使用其他数据库时,应用侧需要自行引入对应 database/sql 驱动,并验证字段元数据、SQL 方言和扫描行为。

操作系统支持

64 位操作系统。

About

另一个 mysql ORM。

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages