Initial commit
This commit is contained in:
commit
91ef568a64
168
crud.go
Normal file
168
crud.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package mgocrud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateDocument creates a document from specified model
|
||||||
|
func CreateDocument(db *mgo.Database, m ModelInterface) error {
|
||||||
|
m.PrepareInsert()
|
||||||
|
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
err := c.Insert(m)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDocument gets one document via its id
|
||||||
|
func ReadDocument(db *mgo.Database, m ModelInterface, selector bson.M) error {
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
|
||||||
|
q := c.FindId(m.GetID())
|
||||||
|
if selector != nil {
|
||||||
|
q = q.Select(selector)
|
||||||
|
}
|
||||||
|
err := q.One(m)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PipelineModifierFunction is a function to modify mongodb query
|
||||||
|
type PipelineModifierFunction func(pipeline []bson.M) []bson.M
|
||||||
|
|
||||||
|
// ReadCollection gets the filtered collection of the model
|
||||||
|
func ReadCollection(db *mgo.Database, results interface{}, filter bson.M, selector bson.M, offset int, limit int, sort []string, pipelineModifier PipelineModifierFunction) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// get pointer to model (element of slice in results) to get collection name
|
||||||
|
m := reflect.New(reflect.TypeOf(
|
||||||
|
reflect.Indirect(reflect.ValueOf(results)).Interface(), // get indirection of slice pointer
|
||||||
|
).Elem()).Interface().(ModelInterface) // it must be a ModelInterface here
|
||||||
|
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
|
||||||
|
if pipelineModifier != nil {
|
||||||
|
// search via pipeline
|
||||||
|
|
||||||
|
pipeline := []bson.M{}
|
||||||
|
|
||||||
|
if filter != nil {
|
||||||
|
pipeline = append(pipeline, bson.M{
|
||||||
|
"$match": filter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sort) > 0 {
|
||||||
|
sortM := bson.M{}
|
||||||
|
for _, s := range sort {
|
||||||
|
if strings.HasPrefix(s, "-") {
|
||||||
|
s = s[1:]
|
||||||
|
sortM[s] = -1
|
||||||
|
} else {
|
||||||
|
sortM[s] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spew.Dump(sortM)
|
||||||
|
pipeline = append(pipeline, bson.M{
|
||||||
|
"$sort": sortM,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > 0 {
|
||||||
|
pipeline = append(pipeline, bson.M{
|
||||||
|
"$skip": offset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
pipeline = append(pipeline, bson.M{
|
||||||
|
"$limit": limit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if selector != nil {
|
||||||
|
pipeline = append(pipeline, bson.M{
|
||||||
|
"$project": selector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if pipelineModifier != nil {
|
||||||
|
pipeline = pipelineModifier(pipeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := c.Pipe(pipeline).AllowDiskUse().Iter()
|
||||||
|
err = q.All(results)
|
||||||
|
} else {
|
||||||
|
// search without pipe is faster
|
||||||
|
|
||||||
|
q := c.Find(filter)
|
||||||
|
if selector != nil {
|
||||||
|
q = q.Select(selector)
|
||||||
|
}
|
||||||
|
if len(sort) > 0 {
|
||||||
|
q = q.Sort(sort...)
|
||||||
|
}
|
||||||
|
if offset > 0 {
|
||||||
|
q = q.Skip(offset)
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
q = q.Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = q.All(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCollectionCount gets the count of elements in filtered collection
|
||||||
|
func ReadCollectionCount(db *mgo.Database, m ModelInterface, filter bson.M) (count int, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
return c.Find(filter).Count()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDocument updates a document from specified model
|
||||||
|
func UpdateDocument(db *mgo.Database, m ModelInterface, changes bson.M) error {
|
||||||
|
m.PrepareUpdate()
|
||||||
|
changes["updateTime"] = time.Now()
|
||||||
|
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
err := c.UpdateId(m.GetID(), bson.M{"$set": changes})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertDocument updates a document from specified model or inserts it, of not found
|
||||||
|
func UpsertDocument(db *mgo.Database, m ModelInterface, changes bson.M) error {
|
||||||
|
m.PrepareUpdate()
|
||||||
|
changes["updateTime"] = time.Now()
|
||||||
|
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
_, err := c.Upsert(m, bson.M{"$set": changes})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteDocument deletes one document via its id
|
||||||
|
func DeleteDocument(db *mgo.Database, m ModelInterface) error {
|
||||||
|
c := db.C(GetCollectionName(m))
|
||||||
|
|
||||||
|
err := c.RemoveId(m.GetID())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
65
interface.go
Normal file
65
interface.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package mgocrud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModelInterface is interface for mgo crud operations
|
||||||
|
type ModelInterface interface {
|
||||||
|
PrepareInsert()
|
||||||
|
PrepareUpdate()
|
||||||
|
GetID() *bson.ObjectId
|
||||||
|
SetID(id *bson.ObjectId)
|
||||||
|
GetIdentSelector() (selector bson.M)
|
||||||
|
SetIdent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model is model with default fields
|
||||||
|
type Model struct {
|
||||||
|
ID *bson.ObjectId `json:"id,omitempty" bson:"_id" mapstructure:"id"`
|
||||||
|
InsertTime *time.Time `json:"insertTime,omitempty" bson:"insertTime,omitempty" mapstructure:"Datum"`
|
||||||
|
UpdateTime *time.Time `json:"updateTime,omitempty" bson:"updateTime,omitempty" mapstructure:"Datum"`
|
||||||
|
Ident []string `json:"ident,omitempty" bson:"-" mapstructure:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCollectionName gets name from type
|
||||||
|
func GetCollectionName(m ModelInterface) string {
|
||||||
|
if i, ok := m.(interface {
|
||||||
|
GetCollectionName() string
|
||||||
|
}); ok {
|
||||||
|
return i.GetCollectionName()
|
||||||
|
}
|
||||||
|
return strings.ToLower(
|
||||||
|
reflect.TypeOf(
|
||||||
|
reflect.Indirect(reflect.ValueOf(m)).Interface(),
|
||||||
|
).Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareInsert creates new bson id and insert time
|
||||||
|
func (m *Model) PrepareInsert() {
|
||||||
|
id := bson.NewObjectId()
|
||||||
|
m.ID = &id
|
||||||
|
now := time.Now()
|
||||||
|
m.InsertTime = &now
|
||||||
|
m.UpdateTime = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareUpdate updates UpdateTime
|
||||||
|
func (m *Model) PrepareUpdate() {
|
||||||
|
now := time.Now()
|
||||||
|
m.UpdateTime = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID gets object id
|
||||||
|
func (m *Model) GetID() *bson.ObjectId {
|
||||||
|
return m.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetID sets object id
|
||||||
|
func (m *Model) SetID(id *bson.ObjectId) {
|
||||||
|
m.ID = id
|
||||||
|
}
|
169
lookup.go
Normal file
169
lookup.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package mgocrud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lookup extends results with data for inline structs
|
||||||
|
func Lookup(db *mgo.Database, structField string, results interface{}, selector bson.M) error {
|
||||||
|
|
||||||
|
t := reflect.TypeOf(results)
|
||||||
|
v := reflect.ValueOf(results)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
isSlice := false
|
||||||
|
elIsPtr := false
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
t = t.Elem()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
elIsPtr = true
|
||||||
|
}
|
||||||
|
isSlice = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("%+v is not a struct", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
field, found := t.FieldByName(structField)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("struct field %s not found in struct %+v", structField, t)
|
||||||
|
}
|
||||||
|
fieldType := field.Type
|
||||||
|
fieldIsPtr := false
|
||||||
|
if fieldType.Kind() == reflect.Ptr {
|
||||||
|
fieldType = fieldType.Elem()
|
||||||
|
fieldIsPtr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldID, found := t.FieldByName(structField + "ID")
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("struct field %s not found in struct %+v", structField+"ID", t)
|
||||||
|
}
|
||||||
|
idType := fieldID.Type
|
||||||
|
idIsPtr := false
|
||||||
|
if idType.Kind() == reflect.Ptr {
|
||||||
|
idType = idType.Elem()
|
||||||
|
idIsPtr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if idType != reflect.TypeOf(bson.ObjectId("")) {
|
||||||
|
return fmt.Errorf("field %s in struct %+v is not bson.ObjectId", structField+"ID", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
objectIDs := make(map[bson.ObjectId]interface{})
|
||||||
|
|
||||||
|
// make slice for single elements to use generic code
|
||||||
|
if !isSlice {
|
||||||
|
tmpSlice := []interface{}{
|
||||||
|
results,
|
||||||
|
}
|
||||||
|
v = reflect.ValueOf(tmpSlice)
|
||||||
|
if reflect.TypeOf(results).Kind() == reflect.Ptr {
|
||||||
|
elIsPtr = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := v.Len()
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
elV := v.Index(i)
|
||||||
|
if elIsPtr {
|
||||||
|
elV = elV.Elem()
|
||||||
|
}
|
||||||
|
if elV.Kind() == reflect.Ptr {
|
||||||
|
// needed if tmpSlice -> dont know why double ptr resolv
|
||||||
|
elV = elV.Elem()
|
||||||
|
}
|
||||||
|
lookupID := elV.FieldByName(fieldID.Name)
|
||||||
|
if idIsPtr {
|
||||||
|
lookupID = lookupID.Elem()
|
||||||
|
}
|
||||||
|
objectIDs[lookupID.Interface().(bson.ObjectId)] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lArr := len(objectIDs)
|
||||||
|
if lArr <= 0 {
|
||||||
|
// no entries to map
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sArr := make([]bson.M, lArr, lArr)
|
||||||
|
aI := 0
|
||||||
|
for sID := range objectIDs {
|
||||||
|
sArr[aI] = bson.M{
|
||||||
|
"_id": sID,
|
||||||
|
}
|
||||||
|
aI++
|
||||||
|
}
|
||||||
|
sQuery := bson.M{
|
||||||
|
"$or": sArr,
|
||||||
|
}
|
||||||
|
|
||||||
|
slice := reflect.MakeSlice(reflect.SliceOf(fieldType), 0, 0)
|
||||||
|
x := reflect.New(slice.Type())
|
||||||
|
x.Elem().Set(slice)
|
||||||
|
objectResults := x.Interface()
|
||||||
|
objectSlice := x.Elem()
|
||||||
|
|
||||||
|
objectIDIsPtr := false
|
||||||
|
objectIDField, found := fieldType.FieldByName("ID")
|
||||||
|
objectIDType := objectIDField.Type
|
||||||
|
if found && objectIDType.Kind() == reflect.Ptr {
|
||||||
|
objectIDType = objectIDType.Elem()
|
||||||
|
objectIDIsPtr = true
|
||||||
|
}
|
||||||
|
if !found || objectIDType != reflect.TypeOf(bson.ObjectId("")) {
|
||||||
|
return fmt.Errorf("ID type in objects struct %+v is not bson.ObjectId", fieldType)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ReadCollection(db, objectResults, sQuery, selector, 0, 0, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// map IDs to object for better resolving
|
||||||
|
oLen := objectSlice.Len()
|
||||||
|
for i := 0; i < oLen; i++ {
|
||||||
|
oID := objectSlice.Index(i).FieldByName("ID")
|
||||||
|
if objectIDIsPtr {
|
||||||
|
oID = oID.Elem()
|
||||||
|
}
|
||||||
|
objectIDs[oID.Interface().(bson.ObjectId)] = objectSlice.Index(i).Addr().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < l; i++ {
|
||||||
|
elV := v.Index(i)
|
||||||
|
if elIsPtr {
|
||||||
|
elV = elV.Elem()
|
||||||
|
}
|
||||||
|
if elV.Kind() == reflect.Ptr {
|
||||||
|
// needed if tmpSlice -> dont know why double ptr resolv
|
||||||
|
elV = elV.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
oID := elV.FieldByName(structField + "ID")
|
||||||
|
if idIsPtr {
|
||||||
|
oID = oID.Elem()
|
||||||
|
}
|
||||||
|
objectID := oID.Interface().(bson.ObjectId)
|
||||||
|
object := objectIDs[objectID]
|
||||||
|
|
||||||
|
field := elV.FieldByName(structField)
|
||||||
|
if fieldIsPtr {
|
||||||
|
field.Set(reflect.ValueOf(object))
|
||||||
|
} else {
|
||||||
|
field.Set(reflect.ValueOf(object).Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
108
setup.go
Normal file
108
setup.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package mgocrud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureIndex ensured mongodb index reflecting model struct index tag
|
||||||
|
func EnsureIndex(db *mgo.Database, m ModelInterface) error {
|
||||||
|
col := db.C(GetCollectionName(m))
|
||||||
|
|
||||||
|
mType := reflect.TypeOf(m)
|
||||||
|
|
||||||
|
textFields := []string{}
|
||||||
|
|
||||||
|
var _indexWalk func(t reflect.Type, fieldbase string) error
|
||||||
|
_indexWalk = func(t reflect.Type, fieldbase string) error {
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() == reflect.Struct {
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
indexTag := field.Tag.Get("index")
|
||||||
|
|
||||||
|
bsonField := strings.Split(field.Tag.Get("bson"), ",")[0]
|
||||||
|
if bsonField == "" {
|
||||||
|
bsonField = strings.ToLower(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bsonField != "-" {
|
||||||
|
if indexTag != "" {
|
||||||
|
index := mgo.Index{
|
||||||
|
Background: false,
|
||||||
|
Sparse: false,
|
||||||
|
Unique: false,
|
||||||
|
}
|
||||||
|
for _, indexEl := range strings.Split(indexTag, ",") {
|
||||||
|
switch {
|
||||||
|
case indexEl == "single":
|
||||||
|
index.Key = []string{fieldbase + bsonField}
|
||||||
|
case indexEl == "unique":
|
||||||
|
index.Unique = true
|
||||||
|
case indexEl == "sparse":
|
||||||
|
index.Sparse = true
|
||||||
|
case indexEl == "background":
|
||||||
|
index.Background = true
|
||||||
|
case indexEl == "text":
|
||||||
|
textFields = append(textFields, "$text:"+fieldbase+bsonField)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid index tag for field %s in model %+v", bsonField, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if len(index.Key) > 0 {
|
||||||
|
// fmt.Println(bsonField, index)
|
||||||
|
fmt.Printf("ensure index on collection %s for field %s\n", GetCollectionName(m), bsonField)
|
||||||
|
err := col.EnsureIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fBase := bsonField + "."
|
||||||
|
if field.Anonymous {
|
||||||
|
fBase = ""
|
||||||
|
}
|
||||||
|
err := _indexWalk(field.Type, fBase)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := _indexWalk(mType, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(textFields) > 0 {
|
||||||
|
// fmt.Println("$text", textFields)
|
||||||
|
fmt.Printf("ensure text index on collection %s for fields %v\n", GetCollectionName(m), textFields)
|
||||||
|
err := col.EnsureIndex(mgo.Index{
|
||||||
|
Name: "textindex",
|
||||||
|
Key: textFields,
|
||||||
|
DefaultLanguage: "german",
|
||||||
|
Background: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
28
validator.go
Normal file
28
validator.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package mgocrud
|
||||||
|
|
||||||
|
import (
|
||||||
|
validator "gopkg.in/go-playground/validator.v8"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateObject validates object via validator tag and custom method
|
||||||
|
func ValidateObject(db *mgo.Database, m ModelInterface, changes bson.M) error {
|
||||||
|
// first validate via struct tag
|
||||||
|
validator := validator.New(&validator.Config{
|
||||||
|
TagName: "validator",
|
||||||
|
})
|
||||||
|
err := validator.Struct(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// next execute custom model validator if exists
|
||||||
|
if i, ok := m.(interface {
|
||||||
|
Validate(db *mgo.Database, changes bson.M) error
|
||||||
|
}); ok {
|
||||||
|
return i.Validate(db, changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user