mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
add LeftJoinOnField/InnerJoinOnField/InnerJoinOnField/FieldsPrefix/FieldsExPrefix for package gdb
This commit is contained in:
@ -408,6 +408,7 @@ type formatWhereInput struct {
|
||||
OmitEmpty bool
|
||||
Schema string
|
||||
Table string
|
||||
Prefix string // Field prefix, eg: "user.", "order.".
|
||||
}
|
||||
|
||||
// formatWhere formats where statement and its arguments for `Where` and `Having` statements.
|
||||
@ -436,6 +437,7 @@ func formatWhere(db DB, in formatWhereInput) (newWhere string, newArgs []interfa
|
||||
Args: newArgs,
|
||||
Key: key,
|
||||
Value: value,
|
||||
Prefix: in.Prefix,
|
||||
})
|
||||
}
|
||||
|
||||
@ -462,6 +464,7 @@ func formatWhere(db DB, in formatWhereInput) (newWhere string, newArgs []interfa
|
||||
Key: ketStr,
|
||||
Value: value,
|
||||
OmitEmpty: in.OmitEmpty,
|
||||
Prefix: in.Prefix,
|
||||
})
|
||||
return true
|
||||
})
|
||||
@ -494,6 +497,7 @@ func formatWhere(db DB, in formatWhereInput) (newWhere string, newArgs []interfa
|
||||
Key: foundKey,
|
||||
Value: foundValue,
|
||||
OmitEmpty: in.OmitEmpty,
|
||||
Prefix: in.Prefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -501,18 +505,31 @@ func formatWhere(db DB, in formatWhereInput) (newWhere string, newArgs []interfa
|
||||
default:
|
||||
// Usually a string.
|
||||
var (
|
||||
i = 0
|
||||
whereStr = gconv.String(in.Where)
|
||||
)
|
||||
// Is `whereStr` a field name which composed as a key-value condition?
|
||||
// Eg:
|
||||
// Where("id", []int{}).All() -> SELECT xxx FROM xxx WHERE 0=1
|
||||
// Where("name", "").All() -> SELECT xxx FROM xxx WHERE `name`=''
|
||||
// OmitEmpty().Where("id", []int{}).All() -> SELECT xxx FROM xxx
|
||||
// OmitEmpty().("name", "").All() -> SELECT xxx FROM xxx
|
||||
if in.OmitEmpty && len(in.Args) == 1 && gstr.Count(whereStr, "?") == 0 && utils.IsEmpty(in.Args[0]) {
|
||||
// Where("id", 1)
|
||||
// Where("id", g.Slice{1,2,3})
|
||||
if gregex.IsMatchString(regularFieldNameWithoutDotRegPattern, whereStr) && len(in.Args) == 1 {
|
||||
newArgs = formatWhereKeyValue(formatWhereKeyValueInput{
|
||||
Db: db,
|
||||
Buffer: buffer,
|
||||
Args: newArgs,
|
||||
Key: whereStr,
|
||||
Value: in.Args[0],
|
||||
OmitEmpty: in.OmitEmpty,
|
||||
Prefix: in.Prefix,
|
||||
})
|
||||
in.Args = in.Args[:0]
|
||||
break
|
||||
}
|
||||
// Regular string and parameter place holder handling.
|
||||
// Eg:
|
||||
// Where("id in(?) and name=?", g.Slice{1,2,3}, "john")
|
||||
var (
|
||||
i = 0
|
||||
)
|
||||
for {
|
||||
if i >= len(in.Args) {
|
||||
break
|
||||
@ -612,6 +629,7 @@ type formatWhereKeyValueInput struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
OmitEmpty bool
|
||||
Prefix string // Field prefix, eg: "user.", "order.".
|
||||
}
|
||||
|
||||
// formatWhereKeyValue handles each key-value pair of the parameter map.
|
||||
@ -628,6 +646,9 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []interface{}) {
|
||||
if in.OmitEmpty && holderCount == 0 && gutil.IsEmpty(in.Value) {
|
||||
return in.Args
|
||||
}
|
||||
if in.Prefix != "" && !gstr.Contains(quotedKey, ".") {
|
||||
quotedKey = in.Prefix + "." + quotedKey
|
||||
}
|
||||
if in.Buffer.Len() > 0 {
|
||||
in.Buffer.WriteString(" AND ")
|
||||
}
|
||||
@ -664,7 +685,7 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []interface{}) {
|
||||
in.Buffer.WriteString(quotedKey)
|
||||
}
|
||||
} else {
|
||||
// It also supports "LIKE" statement, which we considers it an operator.
|
||||
// It also supports "LIKE" statement, which we consider it an operator.
|
||||
quotedKey = gstr.Trim(quotedKey)
|
||||
if gstr.Pos(quotedKey, "?") == -1 {
|
||||
like := " LIKE"
|
||||
|
||||
@ -334,6 +334,12 @@ func (m *Model) Page(page, limit int) *Model {
|
||||
//
|
||||
// The parameter `limit1` specifies whether limits querying only one record if m.limit is not set.
|
||||
func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) {
|
||||
var (
|
||||
prefix = ""
|
||||
)
|
||||
if gstr.Contains(m.tables, " JOIN ") {
|
||||
prefix = m.db.GetCore().QuoteWord(m.tablesInit)
|
||||
}
|
||||
if len(m.whereHolder) > 0 {
|
||||
for _, v := range m.whereHolder {
|
||||
switch v.Operator {
|
||||
@ -346,6 +352,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh
|
||||
OmitEmpty: m.option&optionOmitEmptyWhere > 0,
|
||||
Schema: m.schema,
|
||||
Table: m.tables,
|
||||
Prefix: prefix,
|
||||
})
|
||||
if len(newWhere) > 0 {
|
||||
conditionWhere = newWhere
|
||||
@ -363,6 +370,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh
|
||||
OmitEmpty: m.option&optionOmitEmptyWhere > 0,
|
||||
Schema: m.schema,
|
||||
Table: m.tables,
|
||||
Prefix: prefix,
|
||||
})
|
||||
if len(newWhere) > 0 {
|
||||
if len(conditionWhere) == 0 {
|
||||
@ -383,6 +391,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh
|
||||
OmitEmpty: m.option&optionOmitEmptyWhere > 0,
|
||||
Schema: m.schema,
|
||||
Table: m.tables,
|
||||
Prefix: prefix,
|
||||
})
|
||||
if len(newWhere) > 0 {
|
||||
if len(conditionWhere) == 0 {
|
||||
@ -430,6 +439,7 @@ func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWh
|
||||
OmitEmpty: m.option&optionOmitEmptyWhere > 0,
|
||||
Schema: m.schema,
|
||||
Table: m.tables,
|
||||
Prefix: prefix,
|
||||
})
|
||||
if len(havingStr) > 0 {
|
||||
conditionExtra += " HAVING " + havingStr
|
||||
|
||||
@ -17,6 +17,12 @@ import (
|
||||
|
||||
// Fields appends `fieldNamesOrMapStruct` to the operation fields of the model, multiple fields joined using char ','.
|
||||
// The parameter `fieldNamesOrMapStruct` can be type of string/map/*map/struct/*struct.
|
||||
//
|
||||
// Eg:
|
||||
// Fields("id", "name", "age")
|
||||
// Fields([]string{"id", "name", "age"})
|
||||
// Fields(map[string]interface{}{"id":1, "name":"john", "age":18})
|
||||
// Fields(User{ Id: 1, Name: "john", Age: 18})
|
||||
func (m *Model) Fields(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
length := len(fieldNamesOrMapStruct)
|
||||
if length == 0 {
|
||||
@ -52,10 +58,21 @@ func (m *Model) Fields(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
return m
|
||||
}
|
||||
|
||||
// FieldsPrefix performs as function Fields but add extra prefix for each field.
|
||||
func (m *Model) FieldsPrefix(prefix string, fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
model := m.Fields(fieldNamesOrMapStruct...)
|
||||
array := gstr.SplitAndTrim(model.fields, ",")
|
||||
gstr.PrefixArray(array, prefix+".")
|
||||
model.fields = gstr.Join(array, ",")
|
||||
return model
|
||||
}
|
||||
|
||||
// FieldsEx appends `fieldNamesOrMapStruct` to the excluded operation fields of the model,
|
||||
// multiple fields joined using char ','.
|
||||
// Note that this function supports only single table operations.
|
||||
// The parameter `fieldNamesOrMapStruct` can be type of string/map/*map/struct/*struct.
|
||||
//
|
||||
// Also see Fields.
|
||||
func (m *Model) FieldsEx(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
length := len(fieldNamesOrMapStruct)
|
||||
if length == 0 {
|
||||
@ -80,6 +97,15 @@ func (m *Model) FieldsEx(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
return m
|
||||
}
|
||||
|
||||
// FieldsExPrefix performs as function FieldsEx but add extra prefix for each field.
|
||||
func (m *Model) FieldsExPrefix(prefix string, fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
model := m.FieldsEx(fieldNamesOrMapStruct...)
|
||||
array := gstr.SplitAndTrim(model.fieldsEx, ",")
|
||||
gstr.PrefixArray(array, prefix+".")
|
||||
model.fieldsEx = gstr.Join(array, ",")
|
||||
return model
|
||||
}
|
||||
|
||||
// FieldCount formats and appends commonly used field `COUNT(column)` to the select fields of model.
|
||||
func (m *Model) FieldCount(column string, as ...string) *Model {
|
||||
asStr := ""
|
||||
|
||||
@ -25,40 +25,93 @@ func isSubQuery(s string) bool {
|
||||
|
||||
// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
|
||||
// The parameter `table` can be joined table and its joined condition,
|
||||
// and also with its alias name, like:
|
||||
// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").LeftJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// and also with its alias name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Model("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Model("user", "u").LeftJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) LeftJoin(table ...string) *Model {
|
||||
return m.doJoin("LEFT", table...)
|
||||
}
|
||||
|
||||
// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
|
||||
// The parameter `table` can be joined table and its joined condition,
|
||||
// and also with its alias name, like:
|
||||
// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").RightJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// and also with its alias name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("user").RightJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Model("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Model("user", "u").RightJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) RightJoin(table ...string) *Model {
|
||||
return m.doJoin("RIGHT", table...)
|
||||
}
|
||||
|
||||
// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
|
||||
// The parameter `table` can be joined table and its joined condition,
|
||||
// and also with its alias name, like:
|
||||
// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// and also with its alias name。
|
||||
//
|
||||
// Eg:
|
||||
// Model("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Model("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Model("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) InnerJoin(table ...string) *Model {
|
||||
return m.doJoin("INNER", table...)
|
||||
}
|
||||
|
||||
// LeftJoinOnField performs as LeftJoin, but it joins both tables with the same field name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("order").LeftJoinOnField("user", "user_id")
|
||||
// Model("order").LeftJoinOnField("product", "product_id")
|
||||
func (m *Model) LeftJoinOnField(table, field string) *Model {
|
||||
return m.doJoin("LEFT", table, fmt.Sprintf(
|
||||
`%s.%s=%s.%s`,
|
||||
m.tables,
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
m.db.GetCore().QuoteWord(table),
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
))
|
||||
}
|
||||
|
||||
// RightJoinOnField performs as RightJoin, but it joins both tables with the same field name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("order").InnerJoinOnField("user", "user_id")
|
||||
// Model("order").InnerJoinOnField("product", "product_id")
|
||||
func (m *Model) RightJoinOnField(table, field string) *Model {
|
||||
return m.doJoin("RIGHT", table, fmt.Sprintf(
|
||||
`%s.%s=%s.%s`,
|
||||
m.tables,
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
m.db.GetCore().QuoteWord(table),
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
))
|
||||
}
|
||||
|
||||
// InnerJoinOnField performs as InnerJoin, but it joins both tables with the same field name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("order").InnerJoinOnField("user", "user_id")
|
||||
// Model("order").InnerJoinOnField("product", "product_id")
|
||||
func (m *Model) InnerJoinOnField(table, field string) *Model {
|
||||
return m.doJoin("INNER", table, fmt.Sprintf(
|
||||
`%s.%s=%s.%s`,
|
||||
m.tables,
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
m.db.GetCore().QuoteWord(table),
|
||||
m.db.GetCore().QuoteWord(field),
|
||||
))
|
||||
}
|
||||
|
||||
// doJoin does "LEFT/RIGHT/INNER JOIN ... ON ..." statement on the model.
|
||||
// The parameter `table` can be joined table and its joined condition,
|
||||
// and also with its alias name, like:
|
||||
// Model("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Model("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Model("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// and also with its alias name.
|
||||
//
|
||||
// Eg:
|
||||
// Model("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Model("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Model("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// Related issues:
|
||||
// https://github.com/gogf/gf/issues/1024
|
||||
func (m *Model) doJoin(operator string, table ...string) *Model {
|
||||
|
||||
@ -30,10 +30,10 @@ func (tx *TX) Schema(schema string) *Schema {
|
||||
}
|
||||
}
|
||||
|
||||
// Table creates and returns a new ORM model.
|
||||
// Model creates and returns a new ORM model.
|
||||
// The parameter `tables` can be more than one table names, like :
|
||||
// "user", "user u", "user, user_detail", "user u, user_detail ud"
|
||||
func (s *Schema) Table(table string) *Model {
|
||||
func (s *Schema) Model(table string) *Model {
|
||||
var m *Model
|
||||
if s.tx != nil {
|
||||
m = s.tx.Model(table)
|
||||
@ -51,9 +51,3 @@ func (s *Schema) Table(table string) *Model {
|
||||
m.schema = s.schema
|
||||
return m
|
||||
}
|
||||
|
||||
// Model is alias of Core.Table.
|
||||
// See Core.Table.
|
||||
func (s *Schema) Model(table string) *Model {
|
||||
return s.Table(table)
|
||||
}
|
||||
|
||||
@ -2308,31 +2308,31 @@ func Test_Model_Schema2(t *testing.T) {
|
||||
}()
|
||||
// Schema.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
v, err := db.Schema(TestSchema1).Table(table).Value("nickname", "id=2")
|
||||
v, err := db.Schema(TestSchema1).Model(table).Value("nickname", "id=2")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "name_2")
|
||||
|
||||
r, err := db.Schema(TestSchema1).Table(table).Update(g.Map{"nickname": "name_200"}, "id=2")
|
||||
r, err := db.Schema(TestSchema1).Model(table).Update(g.Map{"nickname": "name_200"}, "id=2")
|
||||
t.AssertNil(err)
|
||||
n, _ := r.RowsAffected()
|
||||
t.Assert(n, 1)
|
||||
|
||||
v, err = db.Schema(TestSchema1).Table(table).Value("nickname", "id=2")
|
||||
v, err = db.Schema(TestSchema1).Model(table).Value("nickname", "id=2")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "name_200")
|
||||
|
||||
v, err = db.Schema(TestSchema2).Table(table).Value("nickname", "id=2")
|
||||
v, err = db.Schema(TestSchema2).Model(table).Value("nickname", "id=2")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "name_2")
|
||||
|
||||
v, err = db.Schema(TestSchema1).Table(table).Value("nickname", "id=2")
|
||||
v, err = db.Schema(TestSchema1).Model(table).Value("nickname", "id=2")
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "name_200")
|
||||
})
|
||||
// Schema.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
i := 1000
|
||||
_, err := db.Schema(TestSchema1).Table(table).Insert(g.Map{
|
||||
_, err := db.Schema(TestSchema1).Model(table).Insert(g.Map{
|
||||
"id": i,
|
||||
"passport": fmt.Sprintf(`user_%d`, i),
|
||||
"password": fmt.Sprintf(`pass_%d`, i),
|
||||
@ -2342,11 +2342,11 @@ func Test_Model_Schema2(t *testing.T) {
|
||||
})
|
||||
t.AssertNil(err)
|
||||
|
||||
v, err := db.Schema(TestSchema1).Table(table).Value("nickname", "id=?", i)
|
||||
v, err := db.Schema(TestSchema1).Model(table).Value("nickname", "id=?", i)
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "name_1000")
|
||||
|
||||
v, err = db.Schema(TestSchema2).Table(table).Value("nickname", "id=?", i)
|
||||
v, err = db.Schema(TestSchema2).Model(table).Value("nickname", "id=?", i)
|
||||
t.AssertNil(err)
|
||||
t.Assert(v.String(), "")
|
||||
})
|
||||
86
database/gdb/gdb_z_mysql_model_join_test.go
Normal file
86
database/gdb/gdb_z_mysql_model_join_test.go
Normal file
@ -0,0 +1,86 @@
|
||||
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the MIT License.
|
||||
// If a copy of the MIT was not distributed with this file,
|
||||
// You can obtain one at https://github.com/gogf/gf.
|
||||
|
||||
package gdb_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_Model_LeftJoinOnField(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
db.SetDebug(true)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := db.Model(table1).
|
||||
FieldsPrefix(table1, "*").
|
||||
LeftJoinOnField(table2, "id").
|
||||
Where("id", g.Slice{1, 2}).
|
||||
Order("id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_RightJoinOnField(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
db.SetDebug(true)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := db.Model(table1).
|
||||
FieldsPrefix(table1, "*").
|
||||
RightJoinOnField(table2, "id").
|
||||
Where("id", g.Slice{1, 2}).
|
||||
Order("id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Model_InnerJoinOnField(t *testing.T) {
|
||||
var (
|
||||
table1 = gtime.TimestampNanoStr() + "_table1"
|
||||
table2 = gtime.TimestampNanoStr() + "_table2"
|
||||
)
|
||||
createInitTable(table1)
|
||||
defer dropTable(table1)
|
||||
createInitTable(table2)
|
||||
defer dropTable(table2)
|
||||
|
||||
db.SetDebug(true)
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
r, err := db.Model(table1).
|
||||
FieldsPrefix(table1, "*").
|
||||
InnerJoinOnField(table2, "id").
|
||||
Where("id", g.Slice{1, 2}).
|
||||
Order("id asc").All()
|
||||
t.AssertNil(err)
|
||||
t.Assert(len(r), 2)
|
||||
t.Assert(r[0]["id"], "1")
|
||||
t.Assert(r[1]["id"], "2")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user