RawQuerier 原生查询
语法分析
就 MySQL SELECT 语句来说,ORM 框架能支持全部 语法吗? 显然不能,也不愿意支持全部 , 也不仅仅是 ORM 框架,大多数框架设计的时候,都要考 虑提供兜底的措施,或者提供绕开你的框架的机制。 在 ORM 这里,就是要允许用户手写 SQL,直接绕开 ORM 的各种机制。
开源实例
Beego ORM
Beego ORM 的原生查询接口是,直接让用户传入 sql 语句与对应的参数,然后返回一个原生查询的抽象 RawSeter,该抽象提供了众多的方法支持。
GORM
GORM 也是依赖于用户传 sql 语句与参数,但是返回给用户的是 DB,用户负责调用目标获取结果的接口,例如: Row()、Rows()、Scan() 等方法。
type Result struct {
ID int
Name string
Age int
}
var result Result
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
db.Raw("SELECT id, name, age FROM users WHERE name = ?", 3).Scan(&result)
var age int
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)
var users []User
db.Raw("UPDATE users SET name = ? WHERE age = ? RETURNING id, name", "jinzhu", 20).Scan(&users)
// 使用原生 SQL
row := db.Raw("select name, age, email from users where name = ?", "jinzhu").Row()
row.Scan(&name, &age, &email)
db.Exec("DROP TABLE users")
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
// Exec with SQL Expression
db.Exec("UPDATE users SET money = ? WHERE name = ?", gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")
API 设计
本文主要支持中间这种。 第三种用户可以直接使用 sql.DB,都用不着 ORM 框架
var _ Querier[any] = &RawQuerier[any]{}
// RawQuerier 原生查询器
type RawQuerier[T any] struct {
core
sess session
sql string
args []any
}
func (r *RawQuerier[T]) Exec(ctx context.Context) Result {
// TODO implement me
panic("implement me")
}
func (r *RawQuerier[T]) Get(ctx context.Context) (*T, error) {
// TODO implement me
panic("implement me")
}
func (r *RawQuerier[T]) GetMulti(ctx context.Context) ([]*T, error) {
// TODO implement me
panic("implement me")
}
具体实现
其中exec 和 get 两个方法是从我们原本的 Selector 与 Updateor 等实现里面抽取出来的。 目的是为了实现
Executor、Querier 等接口,另外为了兼容 Seletor 等构造模式的构造方式,也必须实现 QueryBuilder 接口。
var _ Querier[any] = &RawQuerier[any]{}
// RawQuerier 原生查询器
type RawQuerier[T any] struct {
core
sess session
sql string
args []any
}
// RawQuery 创建一个 RawQuerier 实例
// 泛型参数 T 是目标类型。
// 例如,如果查询 User 的数据,那么 T 就是 User
func RawQuery[T any](sess session, sql string, args ...any) *RawQuerier[T] {
return &RawQuerier[T]{
core: sess.getCore(),
sess: sess,
sql: sql,
args: args,
}
}
func (r *RawQuerier[T]) Build() (*Query, error) {
return &Query{
SQL: r.sql,
Args: r.args,
}, nil
}
func (r *RawQuerier[T]) Get(ctx context.Context) (*T, error) {
// 当通过 RawQuery 方法调用 Get ,如果 T 是 time.Time, sql.Scanner 的实现,
// 内置类型或者基本类型时, 在这里都会报错,但是这种情况我们认为是可以接受的
// 所以在此将报错忽略,因为基本类型取值用不到 meta 里的数据
model, _ := r.r.Get(new(T))
res := get[T](ctx, r.core, r.sess, &QueryContext{
Builder: r,
Type: "RAW",
Meta: model,
})
if res.Err != nil {
return nil, res.Err
}
return res.Result.(*T), nil
}
func (r *RawQuerier[T]) GetMulti(ctx context.Context) ([]*T, error) {
// 当通过 RawQuery 方法调用 Get ,如果 T 是 time.Time, sql.Scanner 的实现,
// 内置类型或者基本类型时, 在这里都会报错,但是这种情况我们认为是可以接受的
// 所以在此将报错忽略,因为基本类型取值用不到 meta 里的数据
model, _ := r.r.Get(new(T))
res := getMulti[T](ctx, r.core, r.sess, &QueryContext{
Builder: r,
Type: "RAW",
Meta: model,
})
if res.Err != nil {
return nil, res.Err
}
return res.Result.([]*T), nil
}
func (r *RawQuerier[T]) Exec(ctx context.Context) Result {
return exec[T](ctx, r.core, r.sess, &QueryContext{
Type: "RAW",
Builder: r,
})
}
单元测试
func TestRawQuerier_Get(t *testing.T) {
//mockDB, mock, err := sqlmock.New()
mockDB, mock, err := sqlmock.New(
sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
t.Fatal(err)
}
defer func() { _ = mockDB.Close() }()
db, err := OpenDB("mysql", mockDB)
if err != nil {
t.Fatal(err)
}
testCases := []struct {
name string
queryRes func(t *testing.T) any
mockErr error
mockOrder func(mock sqlmock.Sqlmock)
wantErr error
wantVal any
}{
//返回原生基本类型
{
name: "res RawQuery int",
queryRes: func(t *testing.T) any {
queryer := RawQuery[int](db, "SELECT `age` FROM `test_model` LIMIT ?;", 1)
result, err := queryer.Get(context.Background())
require.NoError(t, err)
return result
},
mockOrder: func(mock sqlmock.Sqlmock) {
rows := mock.NewRows([]string{"age"}).AddRow(10)
mock.ExpectQuery("SELECT `age` FROM `test_model` LIMIT ?;").
WithArgs(1).
WillReturnRows(rows)
},
wantVal: func() *int {
val := 10
return &val
}(),
},
{
name: "res RawQuery bytes",
queryRes: func(t *testing.T) any {
queryer := RawQuery[[]byte](db, "SELECT `first_name` FROM `test_model` WHERE `id`=? LIMIT ?;", 1, 1)
result, err := queryer.Get(context.Background())
require.NoError(t, err)
return result
},
mockOrder: func(mock sqlmock.Sqlmock) {
rows := mock.NewRows([]string{"first_name"}).AddRow([]byte("Li"))
mock.ExpectQuery("SELECT `first_name` FROM `test_model` WHERE `id`=? LIMIT ?;").
WithArgs(1, 1).
WillReturnRows(rows)
},
wantVal: func() *[]byte {
val := []byte("Li")
return &val
}(),
},
{
name: "res RawQuery string",
queryRes: func(t *testing.T) any {
queryer := RawQuery[string](db, "SELECT `first_name` FROM `test_model` WHERE `id`=? LIMIT ?;", 1, 1)
result, err := queryer.Get(context.Background())
require.NoError(t, err)
return result
},
mockOrder: func(mock sqlmock.Sqlmock) {
rows := mock.NewRows([]string{"first_name"}).AddRow("Da")
mock.ExpectQuery("SELECT `first_name` FROM `test_model` WHERE `id`=? LIMIT ?;").
WithArgs(1, 1).
WillReturnRows(rows)
},
wantVal: func() *string {
val := "Da"
return &val
}(),
},
{
name: "res RawQuery struct ptr",
queryRes: func(t *testing.T) any {
queryer := RawQuery[TestModel](db, "SELECT `first_name`,`age` FROM `test_model` WHERE `id`=? LIMIT ?;", 1, 1)
result, err := queryer.Get(context.Background())
require.NoError(t, err)
return result
},
mockOrder: func(mock sqlmock.Sqlmock) {
rows := mock.NewRows([]string{"first_name", "age"}).AddRow("Da", 18)
mock.ExpectQuery("SELECT `first_name`,`age` FROM `test_model` WHERE `id`=? LIMIT ?;").
WithArgs(1, 1).
WillReturnRows(rows)
},
wantVal: func() *TestModel {
return &TestModel{
FirstName: "Da",
Age: 18,
}
}(),
},
{
name: "res RawQuery sql.NullString",
queryRes: func(t *testing.T) any {
queryer := RawQuery[sql.NullString](db, "SELECT `last_name` FROM `test_model` WHERE `id`=? LIMIT ?;", 1, 1)
result, err := queryer.Get(context.Background())
require.NoError(t, err)
return result
},
mockOrder: func(mock sqlmock.Sqlmock) {
rows := mock.NewRows([]string{"last_name"}).AddRow([]byte("ming"))
mock.ExpectQuery("SELECT `last_name` FROM `test_model` WHERE `id`=? LIMIT ?;").
WithArgs(1, 1).
WillReturnRows(rows)
},
wantVal: func() *sql.NullString {
return &sql.NullString{String: "ming", Valid: true}
}(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.mockOrder(mock)
res := tc.queryRes(t)
assert.Equal(t, tc.wantVal, res)
})
}
}