mirror of
https://gitee.com/johng/gf.git
synced 2024-12-02 04:07:47 +08:00
add ScanList feature for gdb.Result
This commit is contained in:
parent
1290f42f75
commit
3ae44185f4
@ -13,6 +13,11 @@ func (v *Var) Map(tags ...string) map[string]interface{} {
|
||||
return gconv.Map(v.Val(), tags...)
|
||||
}
|
||||
|
||||
// MapStrAny is like function Map, but implements the interface of MapStrAny.
|
||||
func (v *Var) MapStrAny() map[string]interface{} {
|
||||
return v.Map()
|
||||
}
|
||||
|
||||
// MapStrStr converts and returns <v> as map[string]string.
|
||||
func (v *Var) MapStrStr(tags ...string) map[string]string {
|
||||
return gconv.MapStrStr(v.Val(), tags...)
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -40,3 +41,36 @@ func Test_Struct(t *testing.T) {
|
||||
t.Assert(o.Test, -25)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Var_Attribute_Struct(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Uid int
|
||||
Name string
|
||||
}
|
||||
user := new(User)
|
||||
err := gconv.Struct(
|
||||
g.Map{
|
||||
"uid": gvar.New(1),
|
||||
"name": gvar.New("john"),
|
||||
}, user)
|
||||
t.Assert(err, nil)
|
||||
t.Assert(user.Uid, 1)
|
||||
t.Assert(user.Name, "john")
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type User struct {
|
||||
Uid int
|
||||
Name string
|
||||
}
|
||||
var user *User
|
||||
err := gconv.Struct(
|
||||
g.Map{
|
||||
"uid": gvar.New(1),
|
||||
"name": gvar.New("john"),
|
||||
}, &user)
|
||||
t.Assert(err, nil)
|
||||
t.Assert(user.Uid, 1)
|
||||
t.Assert(user.Name, "john")
|
||||
})
|
||||
}
|
||||
|
@ -8,14 +8,33 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/encoding/gparser"
|
||||
)
|
||||
|
||||
// IsEmpty checks and returns whether <r> is empty.
|
||||
func (r Result) IsEmpty() bool {
|
||||
return r.Len() == 0
|
||||
}
|
||||
|
||||
// Len returns the length of result list.
|
||||
func (r Result) Len() int {
|
||||
return len(r)
|
||||
}
|
||||
|
||||
// Size is alias of function Len.
|
||||
func (r Result) Size() int {
|
||||
return r.Len()
|
||||
}
|
||||
|
||||
// Chunk splits an Result into multiple Results,
|
||||
// the size of each array is determined by <size>.
|
||||
// The last chunk may contain less than size elements.
|
||||
@ -81,7 +100,7 @@ func (r Result) Array(field ...string) []Value {
|
||||
}
|
||||
|
||||
// MapKeyValue converts <r> to a map[string]Value of which key is specified by <key>.
|
||||
// Note that the item value can be type of slice.
|
||||
// Note that the item value may be type of slice.
|
||||
func (r Result) MapKeyValue(key string) map[string]Value {
|
||||
var (
|
||||
s = ""
|
||||
@ -227,7 +246,210 @@ func (r Result) Structs(pointer interface{}) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEmpty checks and returns whether <r> is empty.
|
||||
func (r Result) IsEmpty() bool {
|
||||
return len(r) == 0
|
||||
// ScanList converts <r> to struct slice which contains other complex struct attributes.
|
||||
// Note that the parameter <listPointer> should be type of *[]struct/*[]*struct.
|
||||
func (r Result) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error) {
|
||||
// Necessary checks for parameters.
|
||||
if attributeName == "" {
|
||||
return errors.New(`attributeName should not be empty`)
|
||||
}
|
||||
if len(relation) > 0 {
|
||||
if len(relation) < 2 {
|
||||
return errors.New(`relation name and key should are both necessary`)
|
||||
}
|
||||
if relation[0] == "" || relation[1] == "" {
|
||||
return errors.New(`relation name and key should not be empty`)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(listPointer)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
if reflectKind == reflect.Interface {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
if reflectKind != reflect.Ptr {
|
||||
return fmt.Errorf("parameter should be type of *[]struct/*[]*struct, but got: %v", reflectKind)
|
||||
}
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
if reflectKind != reflect.Slice && reflectKind != reflect.Array {
|
||||
return fmt.Errorf("parameter should be type of *[]struct/*[]*struct, but got: %v", reflectKind)
|
||||
}
|
||||
length := len(r)
|
||||
if length == 0 {
|
||||
// The pointed slice is not empty.
|
||||
if reflectValue.Len() > 0 {
|
||||
// It here checks if it has struct item, which is already initialized.
|
||||
// It then returns error to warn the developer its empty and no conversion.
|
||||
if v := reflectValue.Index(0); v.Kind() != reflect.Ptr {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
}
|
||||
// Do nothing for empty struct slice.
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
arrayValue reflect.Value // Like: []*Entity
|
||||
arrayItemType reflect.Type // Like: *Entity
|
||||
reflectType = reflect.TypeOf(listPointer)
|
||||
)
|
||||
if reflectValue.Len() > 0 {
|
||||
arrayValue = reflectValue
|
||||
} else {
|
||||
arrayValue = reflect.MakeSlice(reflectType.Elem(), length, length)
|
||||
}
|
||||
|
||||
// Slice element item.
|
||||
arrayItemType = arrayValue.Index(0).Type()
|
||||
|
||||
// Relation variables.
|
||||
var (
|
||||
relationDataMap map[string]Value
|
||||
relationFieldName string
|
||||
relationAttrName string
|
||||
)
|
||||
if len(relation) > 0 {
|
||||
array := gstr.Split(relation[1], ":")
|
||||
if len(array) > 1 {
|
||||
// Defined table field to relation attribute name.
|
||||
// Like:
|
||||
// uid:Uid
|
||||
// uid:UserId
|
||||
relationFieldName = array[0]
|
||||
relationAttrName = array[1]
|
||||
} else {
|
||||
relationAttrName = relation[1]
|
||||
// Find the possible map key by given only struct attribute name.
|
||||
// Like:
|
||||
// Uid
|
||||
if k, _ := gutil.MapPossibleItemByKey(r[0].Map(), relation[1]); k != "" {
|
||||
relationFieldName = k
|
||||
}
|
||||
}
|
||||
if relationFieldName != "" {
|
||||
relationDataMap = r.MapKeyValue(relationFieldName)
|
||||
}
|
||||
if len(relationDataMap) == 0 {
|
||||
return fmt.Errorf(`cannot find the relation data map, maybe invalid relation key given: %s`, relation[1])
|
||||
}
|
||||
}
|
||||
// Bind to target attribute.
|
||||
var (
|
||||
ok bool
|
||||
attrValue reflect.Value
|
||||
attrKind reflect.Kind
|
||||
attrType reflect.Type
|
||||
attrField reflect.StructField
|
||||
)
|
||||
if arrayItemType.Kind() == reflect.Ptr {
|
||||
if attrField, ok = arrayItemType.Elem().FieldByName(attributeName); !ok {
|
||||
return fmt.Errorf(`invalid field name: %s`, attributeName)
|
||||
}
|
||||
} else {
|
||||
if attrField, ok = arrayItemType.FieldByName(attributeName); !ok {
|
||||
return fmt.Errorf(`invalid field name: %s`, attributeName)
|
||||
}
|
||||
}
|
||||
attrType = attrField.Type
|
||||
attrKind = attrType.Kind()
|
||||
|
||||
// Bind to relation conditions.
|
||||
var (
|
||||
relationValue reflect.Value
|
||||
relationField reflect.Value
|
||||
)
|
||||
for i := 0; i < arrayValue.Len(); i++ {
|
||||
arrayElemValue := arrayValue.Index(i)
|
||||
// The FieldByName should be called on non-pointer reflect.Value.
|
||||
if arrayElemValue.Kind() == reflect.Ptr {
|
||||
// Like: []*Entity
|
||||
arrayElemValue = arrayElemValue.Elem()
|
||||
if !arrayElemValue.IsValid() {
|
||||
// The element is nil, then create one and set it to the slice.
|
||||
// The "reflect.New(itemType.Elem())" creates a new element and returns the address of it.
|
||||
// For example:
|
||||
// reflect.New(itemType.Elem()) => *Entity
|
||||
// reflect.New(itemType.Elem()).Elem() => Entity
|
||||
arrayElemValue = reflect.New(arrayItemType.Elem()).Elem()
|
||||
arrayValue.Index(i).Set(arrayElemValue.Addr())
|
||||
}
|
||||
} else {
|
||||
// Like: []Entity
|
||||
}
|
||||
attrValue = arrayElemValue.FieldByName(attributeName)
|
||||
if len(relation) > 0 {
|
||||
relationValue = arrayElemValue.FieldByName(relation[0])
|
||||
if relationValue.Kind() == reflect.Ptr {
|
||||
relationValue = relationValue.Elem()
|
||||
}
|
||||
}
|
||||
if len(relationDataMap) > 0 && !relationValue.IsValid() {
|
||||
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
|
||||
}
|
||||
switch attrKind {
|
||||
case reflect.Array, reflect.Slice:
|
||||
if len(relationDataMap) > 0 {
|
||||
relationField = relationValue.FieldByName(relationAttrName)
|
||||
if relationField.IsValid() {
|
||||
if err = gconv.Structs(
|
||||
relationDataMap[gconv.String(relationField.Interface())],
|
||||
attrValue.Addr(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// May be the attribute does not exist yet.
|
||||
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`relationKey should not be empty as field "%s" is slice`, attributeName)
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
e := reflect.New(attrType.Elem()).Elem()
|
||||
if len(relationDataMap) > 0 {
|
||||
relationField = relationValue.FieldByName(relationAttrName)
|
||||
if relationField.IsValid() {
|
||||
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// May be the attribute does not exist yet.
|
||||
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
|
||||
}
|
||||
} else {
|
||||
if err = gconv.Struct(r[i], e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
attrValue.Set(e.Addr())
|
||||
|
||||
case reflect.Struct:
|
||||
e := reflect.New(attrType).Elem()
|
||||
if len(relationDataMap) > 0 {
|
||||
relationField = relationValue.FieldByName(relationAttrName)
|
||||
if relationField.IsValid() {
|
||||
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// May be the attribute does not exist yet.
|
||||
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
|
||||
}
|
||||
} else {
|
||||
if err = gconv.Struct(r[i], e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
attrValue.Set(e)
|
||||
|
||||
case reflect.Map:
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
reflect.ValueOf(listPointer).Elem().Set(arrayValue)
|
||||
return nil
|
||||
}
|
||||
|
@ -56,8 +56,8 @@ func Test_Custom_Driver(t *testing.T) {
|
||||
gdb.AddConfigNode("driver-test", gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3306",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
User: USER,
|
||||
Pass: PASS,
|
||||
Name: "test",
|
||||
Type: customDriverName,
|
||||
Role: "master",
|
||||
|
@ -23,6 +23,8 @@ const (
|
||||
SCHEMA1 = "test1"
|
||||
SCHEMA2 = "test2"
|
||||
PREFIX1 = "gf_"
|
||||
USER = "john"
|
||||
PASS = "Nantish1986!"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -40,8 +42,8 @@ func init() {
|
||||
configNode = gdb.ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3306",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
User: USER,
|
||||
Pass: PASS,
|
||||
Name: parser.GetOpt("name", ""),
|
||||
Type: parser.GetOpt("type", "mysql"),
|
||||
Role: "master",
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
|
||||
const (
|
||||
SCHEMA = "test_internal"
|
||||
USER = "john"
|
||||
PASS = "Nantish1986!"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -32,8 +34,8 @@ func init() {
|
||||
configNode = ConfigNode{
|
||||
Host: "127.0.0.1",
|
||||
Port: "3306",
|
||||
User: "root",
|
||||
Pass: "12345678",
|
||||
User: USER,
|
||||
Pass: PASS,
|
||||
Name: parser.GetOpt("name", ""),
|
||||
Type: parser.GetOpt("type", "mysql"),
|
||||
Role: "master",
|
||||
|
@ -1,59 +0,0 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
)
|
||||
|
||||
// TODO
|
||||
func Test_Table_Join(t *testing.T) {
|
||||
var (
|
||||
tableUser = "user_" + gtime.TimestampMicroStr()
|
||||
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()
|
||||
)
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NOT NULL,
|
||||
PRIMARY KEY (uid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableUser)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
defer dropTable(tableUser)
|
||||
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NOT NULL,
|
||||
PRIMARY KEY (uid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableUserDetail)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
defer dropTable(tableUserDetail)
|
||||
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type EntityUser struct {
|
||||
Uid int
|
||||
Name string
|
||||
}
|
||||
type EntityUserDetail struct {
|
||||
Uid int
|
||||
TrueName string
|
||||
}
|
||||
type Entity struct {
|
||||
User *EntityUser
|
||||
UserDetail *EntityUserDetail
|
||||
}
|
||||
})
|
||||
}
|
290
database/gdb/gdb_z_mysql_relation_test.go
Normal file
290
database/gdb/gdb_z_mysql_relation_test.go
Normal file
@ -0,0 +1,290 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). 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 (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
)
|
||||
|
||||
func Test_Table_Relation(t *testing.T) {
|
||||
var (
|
||||
tableUser = "user_" + gtime.TimestampMicroStr()
|
||||
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()
|
||||
tableUserScores = "user_scores_" + gtime.TimestampMicroStr()
|
||||
)
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(45) NOT NULL,
|
||||
PRIMARY KEY (uid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableUser)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
defer dropTable(tableUser)
|
||||
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
address varchar(45) NOT NULL,
|
||||
PRIMARY KEY (uid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableUserDetail)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
defer dropTable(tableUserDetail)
|
||||
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
uid int(10) unsigned NOT NULL,
|
||||
score int(10) unsigned NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
`, tableUserScores)); err != nil {
|
||||
gtest.Error(err)
|
||||
}
|
||||
defer dropTable(tableUserScores)
|
||||
|
||||
type EntityUser struct {
|
||||
Uid int `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type EntityUserDetail struct {
|
||||
Uid int `json:"uid"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
type EntityUserScores struct {
|
||||
Id int `json:"id"`
|
||||
Uid int `json:"uid"`
|
||||
Score int `json:"score"`
|
||||
}
|
||||
type Entity struct {
|
||||
User *EntityUser
|
||||
UserDetail *EntityUserDetail
|
||||
UserScores []*EntityUserScores
|
||||
}
|
||||
|
||||
// Initialize the data.
|
||||
var err error
|
||||
for i := 1; i <= 5; i++ {
|
||||
// User.
|
||||
_, err = db.Insert(tableUser, g.Map{
|
||||
"uid": i,
|
||||
"name": fmt.Sprintf(`name_%d`, i),
|
||||
})
|
||||
gtest.Assert(err, nil)
|
||||
// Detail.
|
||||
_, err = db.Insert(tableUserDetail, g.Map{
|
||||
"uid": i,
|
||||
"address": fmt.Sprintf(`address_%d`, i),
|
||||
})
|
||||
gtest.Assert(err, nil)
|
||||
// Scores.
|
||||
for j := 1; j <= 5; j++ {
|
||||
_, err = db.Insert(tableUserScores, g.Map{
|
||||
"uid": i,
|
||||
"score": j,
|
||||
})
|
||||
gtest.Assert(err, nil)
|
||||
}
|
||||
}
|
||||
// MapKeyValue.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All()
|
||||
t.Assert(err, nil)
|
||||
t.Assert(all.Len(), 2)
|
||||
t.Assert(len(all.MapKeyValue("uid")), 2)
|
||||
t.Assert(all.MapKeyValue("uid")["3"].Map()["uid"], 3)
|
||||
t.Assert(all.MapKeyValue("uid")["4"].Map()["uid"], 4)
|
||||
all, err = db.Table(tableUserScores).Where("uid", g.Slice{3, 4}).Order("id asc").All()
|
||||
t.Assert(err, nil)
|
||||
t.Assert(all.Len(), 10)
|
||||
t.Assert(len(all.MapKeyValue("uid")), 2)
|
||||
t.Assert(len(all.MapKeyValue("uid")["3"].Slice()), 5)
|
||||
t.Assert(len(all.MapKeyValue("uid")["4"].Slice()), 5)
|
||||
t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[0])["uid"], 3)
|
||||
t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[0])["score"], 1)
|
||||
t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["uid"], 3)
|
||||
t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["score"], 5)
|
||||
})
|
||||
// Result ScanList with struct elements and pointer attributes.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []Entity
|
||||
// User
|
||||
all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All()
|
||||
t.Assert(err, nil)
|
||||
err = all.ScanList(&users, "User")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users), 2)
|
||||
t.Assert(users[0].User, &EntityUser{3, "name_3"})
|
||||
t.Assert(users[1].User, &EntityUser{4, "name_4"})
|
||||
// Detail
|
||||
all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserDetail", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"})
|
||||
t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"})
|
||||
// Scores
|
||||
all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserScores", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users[0].UserScores), 5)
|
||||
t.Assert(len(users[1].UserScores), 5)
|
||||
t.Assert(users[0].UserScores[0].Uid, 3)
|
||||
t.Assert(users[0].UserScores[0].Score, 1)
|
||||
t.Assert(users[0].UserScores[4].Score, 5)
|
||||
t.Assert(users[1].UserScores[0].Uid, 4)
|
||||
t.Assert(users[1].UserScores[0].Score, 1)
|
||||
t.Assert(users[1].UserScores[4].Score, 5)
|
||||
})
|
||||
|
||||
// Result ScanList with pointer elements and pointer attributes.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
var users []*Entity
|
||||
// User
|
||||
all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All()
|
||||
t.Assert(err, nil)
|
||||
err = all.ScanList(&users, "User")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users), 2)
|
||||
t.Assert(users[0].User, &EntityUser{3, "name_3"})
|
||||
t.Assert(users[1].User, &EntityUser{4, "name_4"})
|
||||
// Detail
|
||||
all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserDetail", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"})
|
||||
t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"})
|
||||
// Scores
|
||||
all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserScores", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users[0].UserScores), 5)
|
||||
t.Assert(len(users[1].UserScores), 5)
|
||||
t.Assert(users[0].UserScores[0].Uid, 3)
|
||||
t.Assert(users[0].UserScores[0].Score, 1)
|
||||
t.Assert(users[0].UserScores[4].Score, 5)
|
||||
t.Assert(users[1].UserScores[0].Uid, 4)
|
||||
t.Assert(users[1].UserScores[0].Score, 1)
|
||||
t.Assert(users[1].UserScores[4].Score, 5)
|
||||
})
|
||||
|
||||
// Result ScanList with struct elements and struct attributes.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type EntityUser struct {
|
||||
Uid int `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type EntityUserDetail struct {
|
||||
Uid int `json:"uid"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
type EntityUserScores struct {
|
||||
Id int `json:"id"`
|
||||
Uid int `json:"uid"`
|
||||
Score int `json:"score"`
|
||||
}
|
||||
type Entity struct {
|
||||
User EntityUser
|
||||
UserDetail EntityUserDetail
|
||||
UserScores []EntityUserScores
|
||||
}
|
||||
var users []Entity
|
||||
// User
|
||||
all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All()
|
||||
t.Assert(err, nil)
|
||||
err = all.ScanList(&users, "User")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users), 2)
|
||||
t.Assert(users[0].User, &EntityUser{3, "name_3"})
|
||||
t.Assert(users[1].User, &EntityUser{4, "name_4"})
|
||||
// Detail
|
||||
all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserDetail", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"})
|
||||
t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"})
|
||||
// Scores
|
||||
all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserScores", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users[0].UserScores), 5)
|
||||
t.Assert(len(users[1].UserScores), 5)
|
||||
t.Assert(users[0].UserScores[0].Uid, 3)
|
||||
t.Assert(users[0].UserScores[0].Score, 1)
|
||||
t.Assert(users[0].UserScores[4].Score, 5)
|
||||
t.Assert(users[1].UserScores[0].Uid, 4)
|
||||
t.Assert(users[1].UserScores[0].Score, 1)
|
||||
t.Assert(users[1].UserScores[4].Score, 5)
|
||||
})
|
||||
|
||||
// Result ScanList with pointer elements and struct attributes.
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type EntityUser struct {
|
||||
Uid int `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type EntityUserDetail struct {
|
||||
Uid int `json:"uid"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
type EntityUserScores struct {
|
||||
Id int `json:"id"`
|
||||
Uid int `json:"uid"`
|
||||
Score int `json:"score"`
|
||||
}
|
||||
type Entity struct {
|
||||
User EntityUser
|
||||
UserDetail EntityUserDetail
|
||||
UserScores []EntityUserScores
|
||||
}
|
||||
var users []*Entity
|
||||
|
||||
// User
|
||||
all, err := db.Table(tableUser).Where("uid", g.Slice{3, 4}).Order("uid asc").All()
|
||||
t.Assert(err, nil)
|
||||
err = all.ScanList(&users, "User")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users), 2)
|
||||
t.Assert(users[0].User, &EntityUser{3, "name_3"})
|
||||
t.Assert(users[1].User, &EntityUser{4, "name_4"})
|
||||
// Detail
|
||||
all, err = db.Table(tableUserDetail).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("uid asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserDetail", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(users[0].UserDetail, &EntityUserDetail{3, "address_3"})
|
||||
t.Assert(users[1].UserDetail, &EntityUserDetail{4, "address_4"})
|
||||
// Scores
|
||||
all, err = db.Table(tableUserScores).Where("uid", gutil.ListItemValues(users, "User", "Uid")).Order("id asc").All()
|
||||
gtest.Assert(err, nil)
|
||||
err = all.ScanList(&users, "UserScores", "User", "uid:Uid")
|
||||
t.Assert(err, nil)
|
||||
t.Assert(len(users[0].UserScores), 5)
|
||||
t.Assert(len(users[1].UserScores), 5)
|
||||
t.Assert(users[0].UserScores[0].Uid, 3)
|
||||
t.Assert(users[0].UserScores[0].Score, 1)
|
||||
t.Assert(users[0].UserScores[4].Score, 5)
|
||||
t.Assert(users[1].UserScores[0].Uid, 4)
|
||||
t.Assert(users[1].UserScores[0].Score, 1)
|
||||
t.Assert(users[1].UserScores[4].Score, 5)
|
||||
})
|
||||
}
|
@ -7,11 +7,7 @@
|
||||
package gconv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/internal/json"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// SliceMap is alias of Maps.
|
||||
@ -127,95 +123,3 @@ func MapsDeep(value interface{}, tags ...string) []map[string]interface{} {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
// Structs converts any slice to given struct slice.
|
||||
func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) {
|
||||
return doStructs(params, pointer, false, mapping...)
|
||||
}
|
||||
|
||||
// StructsDeep converts any slice to given struct slice recursively.
|
||||
func StructsDeep(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) {
|
||||
return doStructs(params, pointer, true, mapping...)
|
||||
}
|
||||
|
||||
// doStructs converts any slice to given struct slice.
|
||||
//
|
||||
// The parameter <params> should be type of slice.
|
||||
//
|
||||
// The parameter <pointer> should be type of pointer to slice of struct.
|
||||
// Note that if <pointer> is a pointer to another pointer of type of slice of struct,
|
||||
// it will create the struct/pointer internally.
|
||||
func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...map[string]string) (err error) {
|
||||
if params == nil {
|
||||
return errors.New("params cannot be nil")
|
||||
}
|
||||
if pointer == nil {
|
||||
return errors.New("object pointer cannot be nil")
|
||||
}
|
||||
defer func() {
|
||||
// Catch the panic, especially the reflect operation panics.
|
||||
if e := recover(); e != nil {
|
||||
err = gerror.NewfSkip(1, "%v", e)
|
||||
}
|
||||
}()
|
||||
pointerRv, ok := pointer.(reflect.Value)
|
||||
if !ok {
|
||||
pointerRv = reflect.ValueOf(pointer)
|
||||
if kind := pointerRv.Kind(); kind != reflect.Ptr {
|
||||
return fmt.Errorf("pointer should be type of pointer, but got: %v", kind)
|
||||
}
|
||||
}
|
||||
params = Maps(params)
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(params)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
// If <params> is an empty slice, no conversion.
|
||||
if reflectValue.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
array = reflect.MakeSlice(pointerRv.Type().Elem(), reflectValue.Len(), reflectValue.Len())
|
||||
itemType = array.Index(0).Type()
|
||||
)
|
||||
for i := 0; i < reflectValue.Len(); i++ {
|
||||
if itemType.Kind() == reflect.Ptr {
|
||||
// Slice element is type pointer.
|
||||
e := reflect.New(itemType.Elem()).Elem()
|
||||
if deep {
|
||||
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
array.Index(i).Set(e.Addr())
|
||||
} else {
|
||||
// Slice element is not type of pointer.
|
||||
e := reflect.New(itemType).Elem()
|
||||
if deep {
|
||||
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
array.Index(i).Set(e)
|
||||
}
|
||||
}
|
||||
pointerRv.Elem().Set(array)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("params should be type of slice, but got: %v", reflectKind)
|
||||
}
|
||||
}
|
||||
|
106
util/gconv/gconv_structs.go
Normal file
106
util/gconv/gconv_structs.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright 2020 gf Author(https://github.com/gogf/gf). 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 gconv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Structs converts any slice to given struct slice.
|
||||
func Structs(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) {
|
||||
return doStructs(params, pointer, false, mapping...)
|
||||
}
|
||||
|
||||
// StructsDeep converts any slice to given struct slice recursively.
|
||||
func StructsDeep(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) {
|
||||
return doStructs(params, pointer, true, mapping...)
|
||||
}
|
||||
|
||||
// doStructs converts any slice to given struct slice.
|
||||
//
|
||||
// The parameter <params> should be type of slice.
|
||||
//
|
||||
// The parameter <pointer> should be type of pointer to slice of struct.
|
||||
// Note that if <pointer> is a pointer to another pointer of type of slice of struct,
|
||||
// it will create the struct/pointer internally.
|
||||
func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...map[string]string) (err error) {
|
||||
if params == nil {
|
||||
return errors.New("params cannot be nil")
|
||||
}
|
||||
if pointer == nil {
|
||||
return errors.New("object pointer cannot be nil")
|
||||
}
|
||||
defer func() {
|
||||
// Catch the panic, especially the reflect operation panics.
|
||||
if e := recover(); e != nil {
|
||||
err = gerror.NewfSkip(1, "%v", e)
|
||||
}
|
||||
}()
|
||||
pointerRv, ok := pointer.(reflect.Value)
|
||||
if !ok {
|
||||
pointerRv = reflect.ValueOf(pointer)
|
||||
if kind := pointerRv.Kind(); kind != reflect.Ptr {
|
||||
return fmt.Errorf("pointer should be type of pointer, but got: %v", kind)
|
||||
}
|
||||
}
|
||||
params = Maps(params)
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(params)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
// If <params> is an empty slice, no conversion.
|
||||
if reflectValue.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
array = reflect.MakeSlice(pointerRv.Type().Elem(), reflectValue.Len(), reflectValue.Len())
|
||||
itemType = array.Index(0).Type()
|
||||
)
|
||||
for i := 0; i < reflectValue.Len(); i++ {
|
||||
if itemType.Kind() == reflect.Ptr {
|
||||
// Slice element is type pointer.
|
||||
e := reflect.New(itemType.Elem()).Elem()
|
||||
if deep {
|
||||
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
array.Index(i).Set(e.Addr())
|
||||
} else {
|
||||
// Slice element is not type of pointer.
|
||||
e := reflect.New(itemType).Elem()
|
||||
if deep {
|
||||
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
array.Index(i).Set(e)
|
||||
}
|
||||
}
|
||||
pointerRv.Elem().Set(array)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("params should be type of slice, but got: %v", reflectKind)
|
||||
}
|
||||
}
|
@ -13,26 +13,14 @@ import (
|
||||
// ListItemValues retrieves and returns the elements of all item struct/map with key <key>.
|
||||
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
|
||||
// or else it returns an empty slice.
|
||||
func ListItemValues(list interface{}, key interface{}) (values []interface{}) {
|
||||
// If the given <list> is the most common used slice type []map[string]interface{},
|
||||
// it enhances the performance using type assertion.
|
||||
if l, ok := list.([]map[string]interface{}); ok {
|
||||
if len(l) == 0 {
|
||||
return
|
||||
}
|
||||
if mapKey, ok := key.(string); ok {
|
||||
if _, ok := l[0][mapKey]; !ok {
|
||||
return
|
||||
}
|
||||
values = make([]interface{}, len(l))
|
||||
for k, m := range l {
|
||||
values[k] = m[mapKey]
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// It uses reflect for common checks and converting.
|
||||
//
|
||||
// The parameter <list> supports types like:
|
||||
// []map[string]interface{}
|
||||
// []map[string]sub-map
|
||||
// []struct
|
||||
// []struct:sub-struct
|
||||
// Note that the sub-map/sub-struct makes sense only if the optional parameter <subKey> is given.
|
||||
func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{}) {
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(list)
|
||||
reflectKind = reflectValue.Kind()
|
||||
@ -47,47 +35,74 @@ func ListItemValues(list interface{}, key interface{}) (values []interface{}) {
|
||||
if reflectValue.Len() == 0 {
|
||||
return
|
||||
}
|
||||
var (
|
||||
itemValue reflect.Value
|
||||
givenKeyValue = reflect.ValueOf(key)
|
||||
)
|
||||
for i := 0; i < reflectValue.Len(); i++ {
|
||||
itemValue = reflectValue.Index(i)
|
||||
// If the items are type of interface{}.
|
||||
if itemValue.Kind() == reflect.Interface {
|
||||
itemValue = itemValue.Elem()
|
||||
}
|
||||
if itemValue.Kind() == reflect.Ptr {
|
||||
itemValue = itemValue.Elem()
|
||||
}
|
||||
switch itemValue.Kind() {
|
||||
case reflect.Map:
|
||||
v := itemValue.MapIndex(givenKeyValue)
|
||||
if v.IsValid() {
|
||||
values = append(values, v.Interface())
|
||||
if value, ok := doItemValue(reflectValue.Index(i), key); ok {
|
||||
if len(subKey) > 0 && subKey[0] != nil {
|
||||
if subValue, ok := doItemValue(value, subKey[0]); ok {
|
||||
values = append(values, subValue)
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
// The <mapKey> must be type of string.
|
||||
v := itemValue.FieldByName(givenKeyValue.String())
|
||||
if v.IsValid() {
|
||||
values = append(values, v.Interface())
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ItemValue retrieves and returns its value of which name/attribute specified by <key>.
|
||||
// The parameter <item> can be type of map/*map/struct/*struct.
|
||||
func ItemValue(item interface{}, key interface{}) (value interface{}) {
|
||||
value, _ = doItemValue(item, key)
|
||||
return
|
||||
}
|
||||
|
||||
func doItemValue(item interface{}, key interface{}) (value interface{}, found bool) {
|
||||
var reflectValue reflect.Value
|
||||
if v, ok := item.(reflect.Value); ok {
|
||||
reflectValue = v
|
||||
} else {
|
||||
reflectValue = reflect.ValueOf(item)
|
||||
}
|
||||
reflectKind := reflectValue.Kind()
|
||||
if reflectKind == reflect.Interface {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
var keyValue reflect.Value
|
||||
if v, ok := key.(reflect.Value); ok {
|
||||
keyValue = v
|
||||
} else {
|
||||
keyValue = reflect.ValueOf(key)
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Map:
|
||||
v := reflectValue.MapIndex(keyValue)
|
||||
if v.IsValid() {
|
||||
found = true
|
||||
value = v.Interface()
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
// The <mapKey> must be type of string.
|
||||
v := reflectValue.FieldByName(keyValue.String())
|
||||
if v.IsValid() {
|
||||
found = true
|
||||
value = v.Interface()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key <key>.
|
||||
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
|
||||
// or else it returns an empty slice.
|
||||
func ListItemValuesUnique(list interface{}, key string) []interface{} {
|
||||
values := ListItemValues(list, key)
|
||||
func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{} {
|
||||
values := ListItemValues(list, key, subKey...)
|
||||
if len(values) > 0 {
|
||||
var (
|
||||
ok bool
|
||||
|
@ -35,6 +35,23 @@ func Test_ListItemValues_Map(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ListItemValues_SubKey(t *testing.T) {
|
||||
type Scores struct {
|
||||
Math int
|
||||
English int
|
||||
}
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
listMap := g.List{
|
||||
g.Map{"id": 1, "scores": Scores{100, 60}},
|
||||
g.Map{"id": 2, "scores": Scores{0, 100}},
|
||||
g.Map{"id": 3, "scores": Scores{59, 99}},
|
||||
}
|
||||
t.Assert(gutil.ListItemValues(listMap, "scores", "Math"), g.Slice{100, 0, 59})
|
||||
t.Assert(gutil.ListItemValues(listMap, "scores", "English"), g.Slice{60, 100, 99})
|
||||
t.Assert(gutil.ListItemValues(listMap, "scores", "PE"), g.Slice{})
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ListItemValues_Struct(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
type T struct {
|
||||
|
Loading…
Reference in New Issue
Block a user