first commit
This commit is contained in:
commit
911cdd3019
|
@ -0,0 +1 @@
|
|||
tags
|
|
@ -0,0 +1,191 @@
|
|||
package dbmeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.bit5.ru/backend/db"
|
||||
"git.bit5.ru/backend/errors"
|
||||
"git.bit5.ru/backend/meta"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
type DataCollection struct {
|
||||
Db *db.DBC
|
||||
OwnerId uint32
|
||||
sqlFilter string
|
||||
Items map[uint64]meta.IMetaDataItem
|
||||
itemInstance meta.IMetaDataItem
|
||||
ownerFieldIndex int
|
||||
}
|
||||
|
||||
func (coll *DataCollection) Checkin(item meta.IMetaDataItem) (bool, error) {
|
||||
if coll.itemInstance.CLASS_ID() != item.CLASS_ID() {
|
||||
return false, errors.Errorf("Wrong class element %d for adding to collection %d", item.CLASS_ID(), coll.itemInstance.CLASS_ID())
|
||||
}
|
||||
|
||||
id := item.GetIdValue()
|
||||
//NOTE: it's considered to be OK
|
||||
if id == 0 {
|
||||
return false, nil
|
||||
}
|
||||
coll.Items[item.GetIdValue()] = item
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (coll *DataCollection) Save(ctx context.Context) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "DataCollection.Save")
|
||||
defer span.End()
|
||||
|
||||
fields := coll.GetDbFields()
|
||||
countFields := len(fields)
|
||||
if countFields == 0 {
|
||||
return errors.New("Fields list is empty")
|
||||
}
|
||||
|
||||
countRows := len(coll.Items)
|
||||
if countRows == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
allDataWithoutMask := true
|
||||
for _, item := range coll.Items {
|
||||
bitmaskItem, bitmaskable := item.(meta.IBitmasked)
|
||||
if bitmaskable && bitmaskItem.IsMaskFilled() {
|
||||
allDataWithoutMask = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: performance fix: if none of rows have mask we can save all data by one sql query
|
||||
if allDataWithoutMask {
|
||||
return coll.saveWithoutMask(ctx)
|
||||
}
|
||||
|
||||
return coll.saveByMask(ctx)
|
||||
}
|
||||
|
||||
func (coll *DataCollection) saveByMask(ctx context.Context) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "DataCollection.saveByMask")
|
||||
defer span.End()
|
||||
|
||||
fields := coll.GetDbFields()
|
||||
countFields := len(fields)
|
||||
|
||||
tableName := coll.GetTableName()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("db.sql.table", tableName),
|
||||
)
|
||||
|
||||
for _, item := range coll.Items {
|
||||
data := make([]interface{}, countFields)
|
||||
item.Export(data)
|
||||
|
||||
// enforcing ownerId
|
||||
data[coll.ownerFieldIndex] = coll.OwnerId
|
||||
|
||||
itemFields := make([]string, 0, countFields)
|
||||
itemData := make([]interface{}, 0, countFields)
|
||||
|
||||
bitmaskItem, bitmaskable := item.(meta.IBitmasked)
|
||||
|
||||
for fieldInd, fieldName := range fields {
|
||||
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(fieldInd))
|
||||
if skipAdding {
|
||||
continue
|
||||
}
|
||||
|
||||
itemFields = append(itemFields, fieldName)
|
||||
itemData = append(itemData, data[fieldInd])
|
||||
}
|
||||
|
||||
sqlSmt := createInsertSQLForFields(ctx, tableName, itemFields, 1)
|
||||
_, err := coll.Db.UpdateBySQL(sqlSmt, itemData...).Exec()
|
||||
if err != nil {
|
||||
log_len := len(sqlSmt)
|
||||
if log_len > 200 {
|
||||
log_len = 200
|
||||
}
|
||||
|
||||
return errors.Errorf("%s (%s)", err.Error(), sqlSmt[0:log_len])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (coll *DataCollection) saveWithoutMask(ctx context.Context) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "DataCollection.saveWithoutMask")
|
||||
defer span.End()
|
||||
|
||||
tableName := coll.GetTableName()
|
||||
|
||||
fields := coll.GetDbFields()
|
||||
countFields := len(fields)
|
||||
countRows := len(coll.Items)
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("db.sql.table", tableName),
|
||||
attribute.Int("field_amount", countFields),
|
||||
attribute.Int("item_amount", countRows),
|
||||
)
|
||||
|
||||
offset := 0
|
||||
rows := make([]interface{}, countRows*countFields)
|
||||
for _, item := range coll.Items {
|
||||
start := offset * countFields
|
||||
item.Export(rows[start:(start + countFields)])
|
||||
|
||||
ownerValue := rows[start+coll.ownerFieldIndex]
|
||||
if ownerValue == uint32(0) {
|
||||
rows[start+coll.ownerFieldIndex] = coll.OwnerId
|
||||
} else if ownerValue != coll.OwnerId {
|
||||
return errors.Errorf("Wrong owner_id in %s value %d (!= %d)", tableName, ownerValue, coll.OwnerId)
|
||||
}
|
||||
offset++
|
||||
}
|
||||
|
||||
sqlSmt := createInsertSQLForFields(ctx, tableName, fields, countRows)
|
||||
_, err := coll.Db.UpdateBySQL(sqlSmt, rows...).Exec()
|
||||
if err != nil {
|
||||
return errors.Errorf("error: %s, sql: %s", err.Error(), sqlSmt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (coll *DataCollection) GetDbFields() []string {
|
||||
return coll.itemInstance.GetDbFields()
|
||||
}
|
||||
|
||||
func (coll *DataCollection) GetTableName() string {
|
||||
return coll.itemInstance.GetDbTableName()
|
||||
}
|
||||
|
||||
func (coll *DataCollection) init() error {
|
||||
coll.Items = make(map[uint64]meta.IMetaDataItem)
|
||||
|
||||
coll.ownerFieldIndex = -1
|
||||
ownerField := coll.itemInstance.GetOwnerFieldName()
|
||||
for index, field := range coll.itemInstance.GetDbFields() {
|
||||
if field == ownerField {
|
||||
coll.ownerFieldIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if coll.ownerFieldIndex == -1 {
|
||||
return errors.New("Owner field not found in fields list")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDataCollection(db *db.DBC, ownerId uint32, sqlFilter string, itemInstance meta.IMetaDataItem) (*DataCollection, error) {
|
||||
data := DataCollection{Db: db, OwnerId: ownerId, sqlFilter: sqlFilter, itemInstance: itemInstance}
|
||||
if err := data.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package dbmeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func createInsertSQLForFields(ctx context.Context, table string, fields []string, rowsAmount int) string {
|
||||
fieldsStr := strings.Join(fields, "`,`")
|
||||
valuesStr := "(?" + strings.Repeat(", ?", len(fields)-1) + "),"
|
||||
|
||||
sql := fmt.Sprintf("INSERT INTO `%s` (`%s`) VALUES ", table, fieldsStr)
|
||||
sql += strings.TrimRight(valuesStr+strings.Repeat(valuesStr, rowsAmount-1), ",")
|
||||
sql += " ON DUPLICATE KEY UPDATE "
|
||||
|
||||
for i, field := range fields {
|
||||
if i > 0 {
|
||||
sql += ","
|
||||
}
|
||||
sql += fmt.Sprintf("`%s`=VALUES(`%s`)", field, field)
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package dbmeta
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"git.bit5.ru/backend/db"
|
||||
"git.bit5.ru/backend/errors"
|
||||
"git.bit5.ru/backend/meta"
|
||||
)
|
||||
|
||||
func DeleteMetaStruct(db *db.DBC, metaStruct interface{}, ownerId uint32) error {
|
||||
metaStructValue, metaStructType, err := getValueAndType(metaStruct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fieldsProps, err := getMetastructFieldProps(metaStructValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate through struct fields to find field type struct or slice
|
||||
for i := 0; i < metaStructType.NumField(); i++ {
|
||||
field := metaStructType.Field(i)
|
||||
|
||||
if fieldsProps != nil {
|
||||
if props, ok := fieldsProps[field.Name]; ok {
|
||||
if _, ok := props["db_skip_save"]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldType := field.Type
|
||||
fieldValue := metaStructValue.Field(i)
|
||||
|
||||
// skip unexported fields
|
||||
if len(field.PkgPath) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch fieldType.Kind() {
|
||||
case reflect.Slice:
|
||||
// Try to get reflect.Value of slice underlying element
|
||||
sliceElement := getSliceElementValue(fieldValue)
|
||||
if err := DeleteMetaStruct(db, sliceElement.Addr().Interface(), ownerId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
if err := DeleteMetaStruct(db, fieldValue.Addr().Interface(), ownerId); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
case reflect.UnsafePointer:
|
||||
// Now we don't use these types in meta. If we will - fix it.
|
||||
return errors.Errorf("I don't know what I should do with it: %s", fieldType.Kind())
|
||||
case reflect.Map, reflect.Ptr, reflect.Array:
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isMetaDataItem(metaStructValue) {
|
||||
return nil
|
||||
}
|
||||
|
||||
table, ownerColumn, err := getTableAndOwnerColumn(metaStructValue)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = deleteFromTable(db, table, ownerColumn, ownerId)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func isMetaDataItem(metaStructValue reflect.Value) bool {
|
||||
if !metaStructValue.CanInterface() {
|
||||
return false
|
||||
}
|
||||
_, ok := metaStructValue.Addr().Interface().(meta.IMetaDataItem)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Get value and type of MetaStruct. Only pointer is allowed.
|
||||
func getValueAndType(metaStruct interface{}) (reflect.Value, reflect.Type, error) {
|
||||
metaStructValue := reflect.ValueOf(metaStruct)
|
||||
metaStructType := metaStructValue.Type()
|
||||
|
||||
if metaStructType.Kind() != reflect.Ptr {
|
||||
return metaStructValue, metaStructType, errors.New("Value must be a pointer to a struct")
|
||||
}
|
||||
metaStructValue = metaStructValue.Elem()
|
||||
metaStructType = metaStructType.Elem()
|
||||
|
||||
return metaStructValue, metaStructType, nil
|
||||
}
|
||||
|
||||
// Get fieldsProps from metastruct
|
||||
func getMetastructFieldProps(metaStructValue reflect.Value) (map[string]map[string]string, error) {
|
||||
fieldsProps := make(map[string]map[string]string)
|
||||
|
||||
// Check if value is addressable, because meta.IClassProps' method CLASS_FIELDS_PROPS() has a pointer reciever
|
||||
if !metaStructValue.CanAddr() {
|
||||
return fieldsProps, errors.New("value is not addressable!")
|
||||
}
|
||||
|
||||
if !metaStructValue.CanInterface() {
|
||||
return fieldsProps, errors.New("Interface cannot used!")
|
||||
}
|
||||
|
||||
iMetaClassFieldProps, ok := metaStructValue.Addr().Interface().(meta.IClassProps)
|
||||
if !ok {
|
||||
return fieldsProps, errors.New("value is not meta.IClassProps interface!")
|
||||
}
|
||||
fieldsProps = *iMetaClassFieldProps.CLASS_FIELDS_PROPS()
|
||||
|
||||
return fieldsProps, nil
|
||||
}
|
||||
|
||||
// Returns slice element value to get table data
|
||||
func getSliceElementValue(slice reflect.Value) reflect.Value {
|
||||
// Slice Kind here is a pointer.
|
||||
// First call of Elem() returns type from pointer(?).
|
||||
// Second returns type of slice element
|
||||
sliceElementType := slice.Type().Elem().Elem()
|
||||
// Call Elem() on new value, because type of element type is pointer(?)
|
||||
sliceElementValue := reflect.New(sliceElementType).Elem()
|
||||
|
||||
return sliceElementValue
|
||||
}
|
||||
|
||||
func getTableAndOwnerColumn(metaStructValue reflect.Value) (string, string, error) {
|
||||
|
||||
iDataItem, ok := metaStructValue.Addr().Interface().(meta.IMetaDataItem)
|
||||
if !ok {
|
||||
return "", "", errors.New("Can't convert to interface meta.IMetaDataItem!")
|
||||
}
|
||||
|
||||
return iDataItem.GetDbTableName(), iDataItem.GetOwnerFieldName(), nil
|
||||
}
|
||||
|
||||
func deleteFromTable(db *db.DBC, table string, ownerColumnName string, ownerId uint32) error {
|
||||
_, err := db.DeleteFrom(table).Where(ownerColumnName+" = ?", ownerId).Exec()
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
module git.bit5.ru/backend/dbmeta
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
git.bit5.ru/backend/db v1.2.3
|
||||
git.bit5.ru/backend/errors v1.0.0
|
||||
git.bit5.ru/backend/meta v1.0.0
|
||||
github.com/go-logr/stdr v1.2.2
|
||||
github.com/stretchr/testify v1.8.1
|
||||
go.opentelemetry.io/otel v1.11.1
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
git.bit5.ru/backend/db v1.2.3 h1:FYGUbu7nYMny9CoTU6oLrJuQdn+xG42cAHkWI6JLGgE=
|
||||
git.bit5.ru/backend/db v1.2.3/go.mod h1:2PB9QennBZaH8u5i3IvnuVaWnAK5Ck5kCrJ9+5ZnQp8=
|
||||
git.bit5.ru/backend/dbr v1.2.0 h1:SQndOC9KW/jPGRSFSPYRqmMd+EHEyZHWDO9B+imEEzU=
|
||||
git.bit5.ru/backend/dbr v1.2.0/go.mod h1:3iCyHEkyj2M+lx3fSlv3lC1r5cLZZz+6QLVCml7ZAy8=
|
||||
git.bit5.ru/backend/errors v1.0.0 h1:WWJ0sly44q1HQjN01X75ZAGKZwwY5Ml+XVDXMjCkToA=
|
||||
git.bit5.ru/backend/errors v1.0.0/go.mod h1:75faRwsnpM0Se00/Bh7fysWQXV8oMjNJFQ6f7+r9k3Y=
|
||||
git.bit5.ru/backend/meta v1.0.0 h1:1mZgEoOSA/P+IrnKkoiULpFUFX3JxyxGU6OXVn7j2kY=
|
||||
git.bit5.ru/backend/meta v1.0.0/go.mod h1:g45aleNyhUzd1ieXDZV8GDTxCg2B4FKH+0f3DRc3H+w=
|
||||
git.bit5.ru/backend/msgpack v1.0.0 h1:D7sFCFjSN1ADUaESjrRVIWY9TGVATq5i08eKn0ep6ZE=
|
||||
git.bit5.ru/backend/msgpack v1.0.0/go.mod h1:Syf8E+3pr9z3TropB/eN4PJUekRg5ZD/0sHydHH17r0=
|
||||
git.bit5.ru/backend/mysql v1.0.0 h1:NKxTr5p7vti/qrig6zZtWaDeiLRRgPkMB0Nz7AGGlIY=
|
||||
git.bit5.ru/backend/mysql v1.0.0/go.mod h1:iK3dzIpv9YAGN8Fy6zi1XGUX8R5nl9XQ9NJbwYM7y0w=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/vmihailenco/bufio v0.0.0-20140618134113-fe7b595919de h1:U+I4zEVstMdfNES/2UO8iqkIf214SDMRhdaFTE3A5rA=
|
||||
github.com/vmihailenco/bufio v0.0.0-20140618134113-fe7b595919de/go.mod h1:ghSGoeEoFFkXNguSget72dMA0+OLq3AGZiqRohVojxI=
|
||||
go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
|
||||
go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
|
||||
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
|
||||
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
|
||||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU=
|
||||
gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
|
||||
launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
|
|
@ -0,0 +1,165 @@
|
|||
package dbmeta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"git.bit5.ru/backend/db"
|
||||
"git.bit5.ru/backend/errors"
|
||||
"git.bit5.ru/backend/meta"
|
||||
)
|
||||
|
||||
func LoadMetaStruct(db *db.DBC, data meta.IMetaStruct, ownerId uint32) error {
|
||||
return loadStruct(db, data, ownerId)
|
||||
}
|
||||
|
||||
func loadStruct(db *db.DBC, itemInter interface{}, ownerId uint32) error {
|
||||
refItem := reflect.ValueOf(itemInter)
|
||||
mType := refItem.Type()
|
||||
|
||||
var fprops map[string]map[string]string
|
||||
if refItem.CanInterface() {
|
||||
imeta, _ := refItem.Interface().(meta.IClassProps)
|
||||
if imeta != nil {
|
||||
fprops = *imeta.CLASS_FIELDS_PROPS()
|
||||
}
|
||||
}
|
||||
|
||||
if mType.Kind() == reflect.Ptr {
|
||||
refItem = refItem.Elem()
|
||||
mType = mType.Elem()
|
||||
}
|
||||
|
||||
for i := 0; i < mType.NumField(); i++ {
|
||||
field := mType.Field(i)
|
||||
tfield := field.Type
|
||||
if fprops != nil {
|
||||
if props, ok := fprops[field.Name]; ok {
|
||||
if _, ok := props["db_skip_load"]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// skip unexported fields
|
||||
if len(field.PkgPath) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch tfield.Kind() {
|
||||
case reflect.Slice:
|
||||
if err := loadMetaCollection(db, refItem.Field(i).Addr().Interface(), ownerId); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
|
||||
case reflect.Struct:
|
||||
if err := loadStruct(db, refItem.Field(i).Addr().Interface(), ownerId); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
case reflect.Map:
|
||||
case reflect.Ptr:
|
||||
case reflect.Array:
|
||||
case reflect.UnsafePointer:
|
||||
// Now we don't use these types in meta. If we will - fix it.
|
||||
return errors.Errorf("I don't now what I should do with it: %s", tfield.Kind())
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var row meta.IMetaStruct
|
||||
if refItem.CanInterface() {
|
||||
row = refItem.Addr().Interface().(meta.IMetaStruct)
|
||||
}
|
||||
if row == nil {
|
||||
return errors.Errorf("Couldn't convert to IdataRow: %s", mType.Name())
|
||||
}
|
||||
|
||||
props := *row.CLASS_PROPS()
|
||||
if _, ok := props["POD"]; !ok {
|
||||
// skip struct
|
||||
return nil
|
||||
}
|
||||
|
||||
if ownerField, ok := props["owner"]; ok {
|
||||
field := refItem.FieldByName(strings.Title(ownerField))
|
||||
if !field.IsValid() {
|
||||
return errors.Errorf("Owner field \"%s\" is not found in struct \"%s\"", ownerField, mType.Name())
|
||||
}
|
||||
field.Set(reflect.ValueOf(ownerId))
|
||||
}
|
||||
|
||||
if pkeyField, ok := props["pkey"]; ok {
|
||||
field := refItem.FieldByName(strings.Title(pkeyField))
|
||||
if !field.IsValid() {
|
||||
return errors.Errorf("Pkey field \"%s\" is not found in struct \"%s\"", pkeyField, mType.Name())
|
||||
}
|
||||
err := FindOne(db, row, fmt.Sprintf("WHERE `%s`=?", pkeyField), []interface{}{field.Interface()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refItem.Set(reflect.ValueOf(row).Elem())
|
||||
} else {
|
||||
return errors.Errorf("Struct \"%s\" doesn't have pkey field", mType.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadMetaMStruct(db *db.DBC, data interface{}, ownerId uint32) error {
|
||||
return loadMetaCollection(db, data, ownerId)
|
||||
}
|
||||
|
||||
func loadMetaCollection(db *db.DBC, dataItem interface{}, ownerId uint32) error {
|
||||
var err error
|
||||
|
||||
refSlice := reflect.ValueOf(dataItem)
|
||||
if refSlice.Kind() == reflect.Ptr {
|
||||
refSlice = refSlice.Elem()
|
||||
}
|
||||
if refSlice.Kind() != reflect.Slice {
|
||||
return errors.Errorf("It isn't slice: %s", refSlice.Kind())
|
||||
}
|
||||
|
||||
sliceItem := reflect.New(refSlice.Type().Elem().Elem())
|
||||
item := sliceItem.Interface().(meta.IMetaDataItem)
|
||||
if item == nil {
|
||||
return errors.Errorf("Couldn't convert to IMetaDataItem: %s", refSlice.Type().Elem())
|
||||
}
|
||||
|
||||
ownerFieldIndex := -1
|
||||
ownerField := item.GetOwnerFieldName()
|
||||
for index, field := range item.GetDbFields() {
|
||||
if field == ownerField {
|
||||
ownerFieldIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if ownerFieldIndex == -1 {
|
||||
return errors.New("Owner field not found in fields list")
|
||||
}
|
||||
|
||||
sql := "SELECT `" + strings.Join(item.GetDbFields(), "`, `") + "` FROM " + "`" + item.GetDbTableName() + "`" +
|
||||
" WHERE `" + ownerField + "`=" + fmt.Sprintf("%d", ownerId)
|
||||
|
||||
_, err = db.SelectBySQL(sql).LoadStructs(refSlice.Addr().Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindOne(dbc *db.DBC, dataItem meta.IMetaStruct, appendSQL string, params []interface{}) error {
|
||||
info, err := makeDataRowInfo(dataItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sql := "SELECT `" + strings.Join(info.fields, "`, `") + "` FROM `" + info.tableName + "` " + appendSQL
|
||||
err = dbc.SelectBySQL(sql, params...).LoadStruct(dataItem)
|
||||
|
||||
if db.IsNotFoundError(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,493 @@
|
|||
package dbmeta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"git.bit5.ru/backend/db"
|
||||
"git.bit5.ru/backend/errors"
|
||||
"git.bit5.ru/backend/meta"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
|
||||
)
|
||||
|
||||
const tracerName = "game/dbmeta"
|
||||
|
||||
var tracer = otel.Tracer(tracerName)
|
||||
|
||||
type SaveIncompatibleError struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e *SaveIncompatibleError) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
type dataRowInfo struct {
|
||||
mType reflect.Type
|
||||
data meta.IMetaStruct
|
||||
pkey string
|
||||
tableName string
|
||||
fields []string
|
||||
mapFields map[int]int
|
||||
}
|
||||
|
||||
type RemovedIds struct {
|
||||
//class_id => []ids
|
||||
ids map[uint32][]uint64
|
||||
}
|
||||
|
||||
func SaveRow(ctx context.Context, db *db.DBC, dataItem meta.IMetaStruct) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "SaveRow")
|
||||
defer span.End()
|
||||
|
||||
info, err := makeDataRowInfo(dataItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := make([]string, 0, len(info.fields))
|
||||
params := make([]interface{}, 0, len(info.fields))
|
||||
elem := reflect.ValueOf(dataItem).Elem()
|
||||
|
||||
var pkey *reflect.Value
|
||||
|
||||
bitmaskItem, bitmaskable := dataItem.(meta.IBitmasked)
|
||||
|
||||
for fieldIdx, fieldNum := range info.mapFields {
|
||||
value := elem.Field(fieldNum)
|
||||
if info.fields[fieldIdx] == info.pkey && isEmptyField(value) {
|
||||
pkey = &value
|
||||
continue
|
||||
}
|
||||
|
||||
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(fieldIdx))
|
||||
if skipAdding {
|
||||
continue
|
||||
}
|
||||
|
||||
fields = append(fields, info.fields[fieldIdx])
|
||||
params = append(params, value.Interface())
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return errors.New("Fields list is empty")
|
||||
}
|
||||
|
||||
tableName := info.tableName
|
||||
sqlSmt := createInsertSQLForFields(ctx, tableName, fields, 1 /*one_row*/)
|
||||
|
||||
span.SetAttributes(
|
||||
semconv.DBSQLTableKey.String(tableName),
|
||||
semconv.DBStatementKey.String(sqlSmt),
|
||||
)
|
||||
|
||||
updateBuilder := db.UpdateBySQL(sqlSmt, params...)
|
||||
|
||||
res, err := updateBuilder.Exec()
|
||||
if err != nil {
|
||||
if len(sqlSmt) > 200 {
|
||||
sqlSmt = sqlSmt[0:200]
|
||||
}
|
||||
return errors.Errorf("%s (%s)", err.Error(), sqlSmt)
|
||||
}
|
||||
|
||||
insertId, _ := res.LastInsertId()
|
||||
if pkey != nil && insertId != 0 {
|
||||
pkey.Set(reflect.ValueOf(uint32(insertId)).Convert(pkey.Type()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveMetaRootStruct(
|
||||
ctx context.Context,
|
||||
db *db.DBC,
|
||||
data meta.IMetaStruct,
|
||||
ownerId uint32,
|
||||
removedIds meta.IRemovedIds,
|
||||
deltaSave bool,
|
||||
) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "SaveMetaRootStruct")
|
||||
defer span.End()
|
||||
|
||||
dataItem := reflect.ValueOf(data)
|
||||
mType := dataItem.Type()
|
||||
if mType.Kind() == reflect.Ptr {
|
||||
dataItem = dataItem.Elem()
|
||||
mType = mType.Elem()
|
||||
}
|
||||
|
||||
var fprops map[string]map[string]string
|
||||
if dataItem.Addr().CanInterface() {
|
||||
imeta, _ := dataItem.Addr().Interface().(meta.IClassProps)
|
||||
if imeta != nil {
|
||||
fprops = *imeta.CLASS_FIELDS_PROPS()
|
||||
}
|
||||
}
|
||||
|
||||
bitmaskItem, bitmaskable := data.(meta.IBitmasked)
|
||||
|
||||
numField := mType.NumField()
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("mType.name", mType.Name()),
|
||||
attribute.Int("mType.numField", numField),
|
||||
)
|
||||
|
||||
for i := 0; i < numField; i++ {
|
||||
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(i))
|
||||
if skipAdding {
|
||||
continue
|
||||
}
|
||||
|
||||
field := mType.Field(i)
|
||||
tfield := field.Type
|
||||
// Checking if this field should be skipped
|
||||
if fprops != nil {
|
||||
if props, ok := fprops[field.Name]; ok {
|
||||
if _, ok := props["db_skip_save"]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// skip unexported fields
|
||||
if len(field.PkgPath) > 0 {
|
||||
continue
|
||||
}
|
||||
switch tfield.Kind() {
|
||||
case reflect.Slice:
|
||||
if err := SaveMetaCollection(ctx, db, dataItem.Field(i), ownerId, removedIds, deltaSave); err != nil {
|
||||
return errors.WithMessagef(err, "Can not execute SaveMetaRootStruct. Got error from SaveMetaCollection. ownerId: %d.", ownerId)
|
||||
}
|
||||
break
|
||||
case reflect.Struct:
|
||||
if err := saveStruct(ctx, db, dataItem.Field(i), ownerId); err != nil {
|
||||
return errors.WithMessagef(err, "Can not execute SaveMetaRootStruct. Got error from saveStruct. ownerId: %d.", ownerId)
|
||||
}
|
||||
break
|
||||
case reflect.Map:
|
||||
case reflect.Ptr:
|
||||
case reflect.Array:
|
||||
case reflect.UnsafePointer:
|
||||
return errors.Errorf("Unsupported type: %s", tfield.Kind())
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveStruct(ctx context.Context, db *db.DBC, dataItem reflect.Value, ownerId uint32) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "saveStruct")
|
||||
defer span.End()
|
||||
|
||||
mType := dataItem.Type()
|
||||
if mType.Kind() == reflect.Ptr {
|
||||
dataItem = dataItem.Elem()
|
||||
mType = mType.Elem()
|
||||
}
|
||||
|
||||
var row meta.IMetaStruct
|
||||
if dataItem.Addr().CanInterface() {
|
||||
row, _ = dataItem.Addr().Interface().(meta.IMetaStruct)
|
||||
}
|
||||
if row == nil {
|
||||
return errors.Errorf("Couldn't convert to IMetaStruct: %s", mType.Name())
|
||||
}
|
||||
|
||||
props := *row.CLASS_PROPS()
|
||||
if ownerField, ok := props["owner"]; ok {
|
||||
field := dataItem.FieldByName(strings.Title(ownerField))
|
||||
if !field.IsValid() {
|
||||
return errors.Errorf("Owner field \"%s\" is not found in struct \"%s\"", ownerField, mType.Name())
|
||||
}
|
||||
|
||||
// enforcing ownerId
|
||||
if field.CanSet() {
|
||||
field.SetUint(uint64(ownerId))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return SaveRow(ctx, db, row)
|
||||
}
|
||||
|
||||
func SaveMetaCollection(ctx context.Context, db *db.DBC, slice reflect.Value, ownerId uint32, removedIds meta.IRemovedIds, deltaSave bool) error {
|
||||
|
||||
ctx, span := tracer.Start(ctx, "SaveMetaCollection")
|
||||
defer span.End()
|
||||
|
||||
if slice.Type().Kind() != reflect.Slice {
|
||||
return errors.Errorf("It isn't slice: %s", slice.Type().Kind())
|
||||
}
|
||||
sliceItem := reflect.New(slice.Type().Elem().Elem())
|
||||
if sliceItem.Type().Kind() != reflect.Ptr {
|
||||
sliceItem = sliceItem.Addr()
|
||||
}
|
||||
|
||||
var row meta.IMetaStruct
|
||||
if sliceItem.CanInterface() {
|
||||
row, _ = sliceItem.Interface().(meta.IMetaStruct)
|
||||
}
|
||||
|
||||
if row == nil {
|
||||
return errors.Errorf("Couldn't convert to IMetaStruct: %s", sliceItem.Kind())
|
||||
}
|
||||
|
||||
item, _ := sliceItem.Interface().(meta.IMetaDataItem)
|
||||
if item == nil {
|
||||
return errors.Errorf("Couldn't convert to IMetaDataItem: %s", slice.Type())
|
||||
}
|
||||
|
||||
if !deltaSave {
|
||||
cond := fmt.Sprintf("`%s`=%d ", item.GetOwnerFieldName(), ownerId)
|
||||
_, err := db.DeleteFrom(item.GetDbTableName()).Where(cond).Exec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
collection, err := NewDataCollection(db, ownerId, "", item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for ind := 0; ind < slice.Len(); ind++ {
|
||||
ind_item, err := convertToIdataItem(slice.Index(ind))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// NOTE: we don't check here for error on purpose, here it's considered to be OK
|
||||
collection.Checkin(ind_item)
|
||||
}
|
||||
|
||||
if err := collection.Save(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if removedIds != nil {
|
||||
classId := item.CLASS_ID()
|
||||
|
||||
if removedIds.HasList(classId) {
|
||||
err := deleteByIds(db, ownerId, item, removedIds.GetList(classId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteByIds(db *db.DBC, ownerId uint32, item meta.IMetaDataItem, removedIds []uint64) error {
|
||||
var cond string
|
||||
|
||||
if item.GetOwnerFieldName() == item.GetIdFieldName() {
|
||||
cond = fmt.Sprintf("`%s`=%d ", item.GetOwnerFieldName(), ownerId)
|
||||
} else {
|
||||
if len(removedIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]string, 0, len(removedIds))
|
||||
for _, id := range removedIds {
|
||||
ids = append(ids, fmt.Sprintf("%d", id))
|
||||
}
|
||||
cond = fmt.Sprintf(
|
||||
"`%s`=%d AND `%s` IN(%s)",
|
||||
item.GetOwnerFieldName(),
|
||||
ownerId,
|
||||
item.GetIdFieldName(),
|
||||
strings.Join(ids, ","))
|
||||
}
|
||||
|
||||
_, err := db.DeleteFrom(item.GetDbTableName()).Where(cond).Exec()
|
||||
return err
|
||||
}
|
||||
|
||||
func convertToIdataItem(elem reflect.Value) (meta.IMetaDataItem, error) {
|
||||
mType := elem.Type()
|
||||
if mType.Kind() != reflect.Ptr {
|
||||
elem = elem.Addr()
|
||||
mType = elem.Type()
|
||||
}
|
||||
item, _ := elem.Interface().(meta.IMetaDataItem)
|
||||
if item == nil {
|
||||
return nil, errors.Errorf("Couldn't convert to IMetaDataItem: %s", mType.Name())
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func convertToIdataRow(elem reflect.Value) (meta.IMetaStruct, error) {
|
||||
mType := elem.Type()
|
||||
if mType.Kind() != reflect.Ptr {
|
||||
elem = elem.Addr()
|
||||
mType = elem.Type()
|
||||
}
|
||||
item, _ := elem.Interface().(meta.IMetaStruct)
|
||||
if item == nil {
|
||||
return nil, errors.Errorf("Couldn't convert to IdataRow: %s", mType.Name())
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func makeDataRowInfo(data meta.IMetaStruct) (*dataRowInfo, error) {
|
||||
info := new(dataRowInfo)
|
||||
info.mType = reflect.TypeOf(data).Elem()
|
||||
props := *data.CLASS_PROPS()
|
||||
tableName, ok := props["table"]
|
||||
info.tableName = tableName
|
||||
if !ok || len(info.tableName) == 0 {
|
||||
return nil, errors.Errorf("Prop 'table' is empty (%s)", info.mType.Name())
|
||||
}
|
||||
pkey, ok := props["pkey"]
|
||||
info.pkey = ""
|
||||
if ok {
|
||||
info.pkey = pkey
|
||||
}
|
||||
info.fields = data.CLASS_FIELDS()
|
||||
info.mapFields = make(map[int]int, len(info.fields))
|
||||
for ind, field := range info.fields {
|
||||
found := false
|
||||
for i := 0; i < info.mType.NumField(); i++ {
|
||||
if strings.Title(field) == info.mType.Field(i).Name {
|
||||
found = true
|
||||
info.mapFields[ind] = i
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, errors.Errorf("Field %s[%d] not found in struct %s", field, ind, info.mType.Name())
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func isEmptyField(v reflect.Value) bool {
|
||||
vint := reflect.ValueOf(int64(0))
|
||||
vstr := reflect.ValueOf("")
|
||||
if v.Type().ConvertibleTo(vint.Type()) {
|
||||
return reflect.DeepEqual(v.Convert(vint.Type()).Interface(), vint.Interface())
|
||||
}
|
||||
return reflect.DeepEqual(v.Interface(), vstr.Interface())
|
||||
}
|
||||
|
||||
func (rids *RemovedIds) init() {
|
||||
if rids.ids == nil {
|
||||
rids.ids = make(map[uint32][]uint64)
|
||||
}
|
||||
}
|
||||
|
||||
func (rids *RemovedIds) GetList(classId uint32) []uint64 {
|
||||
rids.init()
|
||||
if list, ok := rids.ids[classId]; ok {
|
||||
return list
|
||||
}
|
||||
return []uint64{}
|
||||
}
|
||||
|
||||
func (rids *RemovedIds) Add(classId uint32, id uint64) {
|
||||
rids.init()
|
||||
if _, ok := rids.ids[classId]; !ok {
|
||||
rids.ids[classId] = []uint64{}
|
||||
}
|
||||
rids.ids[classId] = append(rids.ids[classId], id)
|
||||
}
|
||||
|
||||
func (rids *RemovedIds) HasList(classId uint32) bool {
|
||||
rids.init()
|
||||
_, ok := rids.ids[classId]
|
||||
return ok
|
||||
}
|
||||
|
||||
func GetChangedRootStructFields(data meta.IMetaStruct, fieldNames []string) map[string]reflect.Value {
|
||||
var fields = make(map[string]reflect.Value)
|
||||
|
||||
dataItem := reflect.ValueOf(data)
|
||||
mType := dataItem.Type()
|
||||
if mType.Kind() == reflect.Ptr {
|
||||
dataItem = dataItem.Elem()
|
||||
mType = mType.Elem()
|
||||
}
|
||||
|
||||
bitmaskItem, bitmaskable := data.(meta.IBitmasked)
|
||||
|
||||
for i := 0; i < mType.NumField(); i++ {
|
||||
fieldName := mType.Field(i).Name
|
||||
|
||||
if !sliceOfStringsContains(fieldNames, fieldName) {
|
||||
continue
|
||||
}
|
||||
|
||||
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(i))
|
||||
if skipAdding {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[fieldName] = dataItem.Field(i)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func sliceOfStringsContains(slice []string, v string) bool {
|
||||
for _, item := range slice {
|
||||
if item == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetChangedStructFields(dataItem reflect.Value, fieldNamesFilter func(string) bool) (map[string]reflect.Value, error) {
|
||||
var fields = make(map[string]reflect.Value)
|
||||
|
||||
mType := dataItem.Type()
|
||||
if mType.Kind() == reflect.Ptr {
|
||||
dataItem = dataItem.Elem()
|
||||
mType = mType.Elem()
|
||||
}
|
||||
|
||||
var row meta.IMetaStruct
|
||||
if dataItem.Addr().CanInterface() {
|
||||
row, _ = dataItem.Addr().Interface().(meta.IMetaStruct)
|
||||
}
|
||||
if row == nil {
|
||||
return nil, errors.Errorf("Couldn't convert to IMetaStruct: %s", mType.Name())
|
||||
}
|
||||
|
||||
info, err := makeDataRowInfo(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
elem := reflect.ValueOf(row).Elem()
|
||||
bitmaskItem, bitmaskable := row.(meta.IBitmasked)
|
||||
|
||||
for fieldIdx, fieldNum := range info.mapFields {
|
||||
fieldName := info.fields[fieldIdx]
|
||||
if fieldNamesFilter != nil && !fieldNamesFilter(fieldName) {
|
||||
continue
|
||||
}
|
||||
|
||||
value := elem.Field(fieldNum)
|
||||
|
||||
if fieldName == info.pkey && isEmptyField(value) {
|
||||
continue
|
||||
}
|
||||
|
||||
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(fieldIdx))
|
||||
if skipAdding {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[fieldName] = value
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
|
@ -0,0 +1,570 @@
|
|||
package dbmeta_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"git.bit5.ru/backend/dbmeta"
|
||||
"git.bit5.ru/backend/db"
|
||||
"git.bit5.ru/backend/meta"
|
||||
|
||||
"github.com/go-logr/stdr"
|
||||
)
|
||||
|
||||
var logger = stdr.New(log.New(os.Stdout, "", log.Lshortfile))
|
||||
|
||||
//TODO create the tables below before tests
|
||||
//CREATE TABLE `player` (
|
||||
// `id` int unsigned NOT NULL,
|
||||
// `client_version` char(16) NOT NULL DEFAULT '',
|
||||
// `reg_time` int unsigned NOT NULL DEFAULT '0',
|
||||
// `name` varchar(255) NOT NULL DEFAULT '',
|
||||
// `utc_delta` tinyint(1) NOT NULL DEFAULT '0',
|
||||
// PRIMARY KEY (`id`)
|
||||
//) ENGINE=InnoDB DEFAULT
|
||||
//
|
||||
// CREATE TABLE `item` (
|
||||
// `player_id` int unsigned NOT NULL DEFAULT '0',
|
||||
// `id` int unsigned NOT NULL DEFAULT '0',
|
||||
// `proto_id` int unsigned NOT NULL DEFAULT '0',
|
||||
// `amount` bigint NOT NULL DEFAULT '0',
|
||||
// PRIMARY KEY (`player_id`,`id`),
|
||||
// KEY `proto_id` (`proto_id`)
|
||||
//) ENGINE=InnoDB DEFAULT
|
||||
|
||||
func getDBC() *db.DBC {
|
||||
s := db.Settings{Host: "127.0.0.1", Port: "3306", User: "root", Pass: "test", Name: "tests_shard_1", Prefix: "tests"}
|
||||
dbc := db.GetDBC(db.OpenPool(s), logger)
|
||||
return dbc
|
||||
}
|
||||
|
||||
func cleanStorage(db *db.DBC) {
|
||||
var tables []string
|
||||
db.SelectBySQL("SHOW TABLES").LoadValues(&tables)
|
||||
for _, t := range tables {
|
||||
_, err := db.DeleteFrom(t).Exec()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRow(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
conn := getDBC()
|
||||
cleanStorage(conn)
|
||||
|
||||
//DataPlayer index
|
||||
//id 0
|
||||
//client_version 1
|
||||
//reg_time 2
|
||||
//utc_delta 3
|
||||
|
||||
player := &DataPlayer{Id: 100, Client_version: "0.0.1", Reg_time: 100, Utc_delta: 1}
|
||||
player.SetFieldChanged(0)
|
||||
player.SetFieldChanged(2)
|
||||
require.NoError(t, dbmeta.SaveRow(ctx, conn, player))
|
||||
|
||||
var item DataPlayer
|
||||
require.NoError(t, dbmeta.LoadMetaStruct(conn, &item, player.Id))
|
||||
|
||||
assert.EqualValues(t, "", item.Client_version)
|
||||
assert.EqualValues(t, 0, item.Utc_delta)
|
||||
assert.EqualValues(t, 100, item.Reg_time)
|
||||
}
|
||||
|
||||
func TestSaveItemCollectionWithMask(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
conn := getDBC()
|
||||
cleanStorage(conn)
|
||||
|
||||
ownerId := uint32(1)
|
||||
|
||||
//DataItem index
|
||||
//id 0
|
||||
//player_id 1
|
||||
//proto_id 2
|
||||
//amount 3
|
||||
|
||||
item1 := &DataItem{Player_id: ownerId, Id: 10, Proto_id: 100, Amount: 1}
|
||||
item1.SetFieldChanged(0)
|
||||
item1.SetFieldChanged(1)
|
||||
item1.SetFieldChanged(2)
|
||||
|
||||
item2 := &DataItem{Player_id: ownerId, Id: 11, Proto_id: 200, Amount: 2}
|
||||
item2.SetFieldChanged(0)
|
||||
item2.SetFieldChanged(1)
|
||||
item2.SetFieldChanged(3)
|
||||
|
||||
var items []*DataItem
|
||||
items = append(items, item1)
|
||||
items = append(items, item2)
|
||||
|
||||
require.NoError(t, dbmeta.SaveMetaCollection(ctx, conn, reflect.ValueOf(items), ownerId, nil, false))
|
||||
|
||||
items = nil
|
||||
require.NoError(t, dbmeta.LoadMetaMStruct(conn, &items, ownerId))
|
||||
|
||||
assert.EqualValues(t, items[0].Player_id, ownerId)
|
||||
assert.EqualValues(t, items[0].Id, 10)
|
||||
assert.EqualValues(t, items[0].Proto_id, 100)
|
||||
assert.EqualValues(t, items[0].Amount, 0)
|
||||
|
||||
assert.EqualValues(t, items[1].Player_id, ownerId)
|
||||
assert.EqualValues(t, items[1].Id, 11)
|
||||
assert.EqualValues(t, items[1].Proto_id, 0)
|
||||
assert.EqualValues(t, items[1].Amount, 2)
|
||||
}
|
||||
|
||||
func TestSaveItemCollectionWithoutMask(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
|
||||
conn := getDBC()
|
||||
cleanStorage(conn)
|
||||
|
||||
ownerId := uint32(1)
|
||||
|
||||
item1 := &DataItem{Player_id: ownerId, Id: 10, Proto_id: 100, Amount: 1}
|
||||
item2 := &DataItem{Player_id: ownerId, Id: 11, Proto_id: 200, Amount: 2}
|
||||
|
||||
var items []*DataItem
|
||||
items = append(items, item1)
|
||||
items = append(items, item2)
|
||||
|
||||
require.NoError(t, dbmeta.SaveMetaCollection(ctx, conn, reflect.ValueOf(items), ownerId, nil, false))
|
||||
|
||||
items = nil
|
||||
require.NoError(t, dbmeta.LoadMetaMStruct(conn, &items, ownerId))
|
||||
|
||||
assert.EqualValues(t, items[0].Player_id, ownerId)
|
||||
assert.EqualValues(t, items[0].Id, 10)
|
||||
assert.EqualValues(t, items[0].Proto_id, 100)
|
||||
assert.EqualValues(t, items[0].Amount, 1)
|
||||
|
||||
assert.EqualValues(t, items[1].Player_id, ownerId)
|
||||
assert.EqualValues(t, items[1].Id, 11)
|
||||
assert.EqualValues(t, items[1].Proto_id, 200)
|
||||
assert.EqualValues(t, items[1].Amount, 2)
|
||||
}
|
||||
|
||||
func TestRemoveIds(t *testing.T) {
|
||||
ids := &dbmeta.RemovedIds{}
|
||||
|
||||
assert.EqualValues(t, []uint64{}, ids.GetList(100))
|
||||
|
||||
ids.Add(100, 1)
|
||||
ids.Add(100, 3)
|
||||
|
||||
ids.Add(200, 1)
|
||||
ids.Add(300, 3)
|
||||
|
||||
assert.EqualValues(t, false, ids.HasList(10))
|
||||
assert.EqualValues(t, true, ids.HasList(100))
|
||||
assert.EqualValues(t, true, ids.HasList(300))
|
||||
|
||||
assert.EqualValues(t, []uint64{1, 3}, ids.GetList(100))
|
||||
assert.EqualValues(t, []uint64{3}, ids.GetList(300))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type DataPlayer struct {
|
||||
|
||||
Id uint32 `json:"id" db:"id"`
|
||||
Client_version string `json:"client_version" db:"client_version"`
|
||||
Reg_time uint32 `json:"reg_time" db:"reg_time"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Utc_delta int32 `json:"utc_delta" db:"utc_delta"`
|
||||
fieldsMask meta.FieldsMask
|
||||
|
||||
}
|
||||
|
||||
var _DataPlayer_class_props map[string]string = map[string]string{"POD": "","bitfields": "","pkey": "id","table": "player","owner": "id","id": "id","cloneable": "",}
|
||||
var _DataPlayer_class_fields []string = []string{"id","client_version","reg_time","name","utc_delta"}
|
||||
var _DataPlayer_fields_props meta.ClassFieldsProps = map[string]map[string]string{"Id" : map[string]string{"optional": "1",},"Client_version" : map[string]string{"strmax": "16","optional": "1",},"Reg_time" : map[string]string{"default": "","optional": "1",},"Name" : map[string]string{"default": "\"\"","optional": "1",},"Utc_delta" : map[string]string{"default": "","optional": "1",}}
|
||||
|
||||
func DataPlayer_CLASS_ID() uint32 {
|
||||
return 74407040
|
||||
}
|
||||
|
||||
type IDataPlayer interface {
|
||||
meta.IMetaStruct
|
||||
PtrDataPlayer() *DataPlayer
|
||||
}
|
||||
|
||||
func (*DataPlayer) CLASS_ID() uint32 {
|
||||
return 74407040
|
||||
}
|
||||
|
||||
func (*DataPlayer) CLASS_NAME() string {
|
||||
return "DataPlayer"
|
||||
}
|
||||
|
||||
func (*DataPlayer) CLASS_PROPS() *map[string]string {
|
||||
return &_DataPlayer_class_props
|
||||
}
|
||||
|
||||
func (*DataPlayer) CLASS_FIELDS() []string {
|
||||
return _DataPlayer_class_fields
|
||||
}
|
||||
|
||||
func (*DataPlayer) CLASS_FIELDS_PROPS() *meta.ClassFieldsProps {
|
||||
return &_DataPlayer_fields_props
|
||||
}
|
||||
|
||||
//convenience getter
|
||||
func PtrDataPlayer(m meta.IMetaStruct) *DataPlayer {
|
||||
p, ok := m.(IDataPlayer)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p.PtrDataPlayer()
|
||||
}
|
||||
|
||||
func (self *DataPlayer) PtrDataPlayer() *DataPlayer {
|
||||
return self
|
||||
}
|
||||
|
||||
func NewDataPlayer() *DataPlayer {
|
||||
item := new (DataPlayer)
|
||||
item.Reset()
|
||||
return item
|
||||
}
|
||||
|
||||
func (self *DataPlayer) Reset() {
|
||||
|
||||
self.Id = 0
|
||||
self.Client_version = ""
|
||||
self.Reg_time = 0
|
||||
self.Name = ""
|
||||
self.Utc_delta = 0
|
||||
self.fieldsMask = meta.FieldsMask{}
|
||||
|
||||
}
|
||||
|
||||
func (self *DataPlayer) Read(reader meta.Reader) error {
|
||||
return meta.ReadStruct(reader, self, "")
|
||||
}
|
||||
|
||||
func (self *DataPlayer) ReadFields(reader meta.Reader) error {
|
||||
self.Reset()
|
||||
|
||||
use_mask, mask, err := reader.TryReadMask()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
self.fieldsMask = mask
|
||||
|
||||
_cont_size, err := reader.GetContainerSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _cont_size < 0 {
|
||||
_cont_size = 0
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(0)) {
|
||||
if err := reader.ReadU32(&self.Id, "id"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(1)) {
|
||||
if err := reader.ReadString(&self.Client_version, "client_version"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(2)) {
|
||||
if err := reader.ReadU32(&self.Reg_time, "reg_time"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(3)) {
|
||||
if err := reader.ReadString(&self.Name, "name"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(4)) {
|
||||
if err := reader.ReadI32(&self.Utc_delta, "utc_delta"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *DataPlayer) Write(writer meta.Writer) error {
|
||||
return meta.WriteStruct(writer, self, "")
|
||||
}
|
||||
|
||||
func (self *DataPlayer) WriteFields(writer meta.Writer) error {
|
||||
|
||||
if err := writer.WriteU32(self.Id, "id"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteString(self.Client_version, "client_version"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteU32(self.Reg_time, "reg_time"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteString(self.Name, "name"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteI32(self.Utc_delta, "utc_delta"); err != nil { return err }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func(self *DataPlayer) HasValue(index uint64) bool {
|
||||
return self.fieldsMask.FieldChanged(index)
|
||||
}
|
||||
func(self *DataPlayer) SetFieldChanged(index uint64) {
|
||||
self.fieldsMask.SetFieldChanged(index)
|
||||
}
|
||||
func(self *DataPlayer) IsMaskFilled() bool {
|
||||
return self.fieldsMask.IsFilled()
|
||||
}
|
||||
func(self *DataPlayer) GetMask() meta.FieldsMask {
|
||||
return self.fieldsMask
|
||||
}
|
||||
|
||||
type DataItem struct {
|
||||
|
||||
Id uint32 `json:"id" db:"id"`
|
||||
Player_id uint32 `json:"player_id" db:"player_id"`
|
||||
Proto_id uint32 `json:"proto_id" db:"proto_id"`
|
||||
Amount int64 `json:"amount" db:"amount"`
|
||||
fieldsMask meta.FieldsMask
|
||||
|
||||
}
|
||||
|
||||
var _DataItem_class_props map[string]string = map[string]string{"POD": "","table": "item","id": "id","owner": "player_id","bitfields": "",}
|
||||
var _DataItem_class_fields []string = []string{"id","player_id","proto_id","amount",}
|
||||
var _DataItem_fields_props meta.ClassFieldsProps = map[string]map[string]string{"Id" : map[string]string{"optional": "1",},"Player_id" : map[string]string{"optional": "1",},"Proto_id" : map[string]string{"obscured": "","optional": "1",},"Amount" : map[string]string{"obscured": "","optional": "1",},}
|
||||
|
||||
func DataItem_CLASS_ID() uint32 {
|
||||
return 263721017
|
||||
}
|
||||
|
||||
type IDataItem interface {
|
||||
meta.IMetaStruct
|
||||
PtrDataItem() *DataItem
|
||||
}
|
||||
|
||||
func (*DataItem) CLASS_ID() uint32 {
|
||||
return 263721017
|
||||
}
|
||||
|
||||
func (*DataItem) CLASS_NAME() string {
|
||||
return "DataItem"
|
||||
}
|
||||
|
||||
func (*DataItem) CLASS_PROPS() *map[string]string {
|
||||
return &_DataItem_class_props
|
||||
}
|
||||
|
||||
func (*DataItem) CLASS_FIELDS() []string {
|
||||
return _DataItem_class_fields
|
||||
}
|
||||
|
||||
func (*DataItem) CLASS_FIELDS_PROPS() *meta.ClassFieldsProps {
|
||||
return &_DataItem_fields_props
|
||||
}
|
||||
|
||||
//convenience getter
|
||||
func PtrDataItem(m meta.IMetaStruct) *DataItem {
|
||||
p, ok := m.(IDataItem)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p.PtrDataItem()
|
||||
}
|
||||
|
||||
func (self *DataItem) PtrDataItem() *DataItem {
|
||||
return self
|
||||
}
|
||||
|
||||
func NewDataItem() *DataItem {
|
||||
item := new (DataItem)
|
||||
item.Reset()
|
||||
return item
|
||||
}
|
||||
|
||||
func (self *DataItem) Reset() {
|
||||
|
||||
self.Id = 0
|
||||
self.Player_id = 0
|
||||
self.Proto_id = 0
|
||||
self.Amount = 0
|
||||
self.fieldsMask = meta.FieldsMask{}
|
||||
|
||||
}
|
||||
|
||||
func (self *DataItem) Read(reader meta.Reader) error {
|
||||
return meta.ReadStruct(reader, self, "")
|
||||
}
|
||||
|
||||
func (self *DataItem) ReadFields(reader meta.Reader) error {
|
||||
self.Reset()
|
||||
|
||||
use_mask, mask, err := reader.TryReadMask()
|
||||
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
self.fieldsMask = mask
|
||||
|
||||
_cont_size, err := reader.GetContainerSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _cont_size < 0 {
|
||||
_cont_size = 0
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(0)) {
|
||||
if err := reader.ReadU32(&self.Id, "id"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(1)) {
|
||||
if err := reader.ReadU32(&self.Player_id, "player_id"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(2)) {
|
||||
if err := reader.ReadU32(&self.Proto_id, "proto_id"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
if _cont_size <= 0 {
|
||||
return nil
|
||||
}
|
||||
if !use_mask {
|
||||
_cont_size--
|
||||
}
|
||||
if !use_mask || (use_mask && self.HasValue(3)) {
|
||||
if err := reader.ReadI64(&self.Amount, "amount"); err != nil { return /*optional*/nil }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *DataItem) Write(writer meta.Writer) error {
|
||||
return meta.WriteStruct(writer, self, "")
|
||||
}
|
||||
|
||||
func (self *DataItem) WriteFields(writer meta.Writer) error {
|
||||
|
||||
if err := writer.WriteU32(self.Id, "id"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteU32(self.Player_id, "player_id"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteU32(self.Proto_id, "proto_id"); err != nil { return err }
|
||||
|
||||
if err := writer.WriteI64(self.Amount, "amount"); err != nil { return err }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func(self *DataItem) HasValue(index uint64) bool {
|
||||
return self.fieldsMask.FieldChanged(index)
|
||||
}
|
||||
func(self *DataItem) SetFieldChanged(index uint64) {
|
||||
self.fieldsMask.SetFieldChanged(index)
|
||||
}
|
||||
func(self *DataItem) IsMaskFilled() bool {
|
||||
return self.fieldsMask.IsFilled()
|
||||
}
|
||||
func(self *DataItem) GetMask() meta.FieldsMask {
|
||||
return self.fieldsMask
|
||||
}
|
||||
|
||||
func (self *DataItem) NewInstance() meta.IMetaDataItem {
|
||||
return NewDataItem()
|
||||
}
|
||||
|
||||
func (self *DataItem) GetDbTableName() string {
|
||||
return "item"
|
||||
}
|
||||
|
||||
func (self *DataItem) GetDbFields() []string {
|
||||
return self.CLASS_FIELDS()
|
||||
}
|
||||
|
||||
func (self *DataItem) GetOwnerFieldName() string {
|
||||
return "player_id"
|
||||
}
|
||||
|
||||
func (self *DataItem) GetIdFieldName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
func (self *DataItem) GetIdValue() uint64 {
|
||||
return uint64(self.Id)
|
||||
}
|
||||
|
||||
func (self *DataItem) Import(data interface{}) {
|
||||
switch data.(type) {
|
||||
case DataItem:
|
||||
{
|
||||
|
||||
row := data.(DataItem)
|
||||
self.Id = row.Id
|
||||
self.Player_id = row.Player_id
|
||||
self.Proto_id = row.Proto_id
|
||||
self.Amount = row.Amount
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (self *DataItem) Export(data []interface{}) {
|
||||
|
||||
data[0] = self.Id
|
||||
data[1] = self.Player_id
|
||||
data[2] = self.Proto_id
|
||||
data[3] = self.Amount
|
||||
}
|
||||
|
Loading…
Reference in New Issue