Initial nested eager load.
This commit is contained in:
parent
8ab617ef71
commit
f2b8f39d47
2 changed files with 189 additions and 37 deletions
106
boil/reflect.go
106
boil/reflect.go
|
@ -14,6 +14,11 @@ var (
|
||||||
bindAccepts = []reflect.Kind{reflect.Ptr, reflect.Slice, reflect.Ptr, reflect.Struct}
|
bindAccepts = []reflect.Kind{reflect.Ptr, reflect.Slice, reflect.Ptr, reflect.Struct}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
loadMethodPrefix = "Load"
|
||||||
|
relationshipStructName = "R"
|
||||||
|
)
|
||||||
|
|
||||||
// BindP executes the query and inserts the
|
// BindP executes the query and inserts the
|
||||||
// result into the passed in object pointer.
|
// result into the passed in object pointer.
|
||||||
// It panics on error. See boil.Bind() documentation.
|
// It panics on error. See boil.Bind() documentation.
|
||||||
|
@ -100,11 +105,13 @@ func (q *Query) BindFast(obj interface{}, titleCases map[string]string) error {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(q.load) == 0 {
|
for _, toLoad := range q.load {
|
||||||
return nil
|
toLoadFragments := strings.Split(toLoad, ".")
|
||||||
|
if err = loadRelationships(q.executor, toLoadFragments, obj, singular); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return q.loadRelationships(obj, singular)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadRelationships dynamically calls the template generated eager load
|
// loadRelationships dynamically calls the template generated eager load
|
||||||
|
@ -119,47 +126,90 @@ func (q *Query) BindFast(obj interface{}, titleCases map[string]string) error {
|
||||||
// - singular is passed in to identify whether or not this was a single object
|
// - singular is passed in to identify whether or not this was a single object
|
||||||
// or a slice that must be loaded into.
|
// or a slice that must be loaded into.
|
||||||
// - obj is the object or slice of objects, always of the type *obj or *[]*obj as per bind.
|
// - obj is the object or slice of objects, always of the type *obj or *[]*obj as per bind.
|
||||||
func (q *Query) loadRelationships(obj interface{}, singular bool) error {
|
//
|
||||||
|
// It takes list of nested relationships to load.
|
||||||
|
func loadRelationships(exec Executor, toLoad []string, obj interface{}, singular bool) error {
|
||||||
typ := reflect.TypeOf(obj).Elem()
|
typ := reflect.TypeOf(obj).Elem()
|
||||||
if !singular {
|
if !singular {
|
||||||
typ = typ.Elem().Elem()
|
typ = typ.Elem().Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
rel, found := typ.FieldByName("R")
|
current := toLoad[0]
|
||||||
// If the users object has no loaded struct, it must be
|
r, found := typ.FieldByName(relationshipStructName)
|
||||||
// a custom object and we should not attempt to load any relationships.
|
// It's possible a Relationship struct doesn't exist on the struct.
|
||||||
if !found {
|
if !found {
|
||||||
return errors.New("load query mod was used but bound struct contained no R field")
|
return errors.Errorf("attempted to load %s but no R struct was found", current)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, relationship := range q.load {
|
// Attempt to find the LoadRelationshipName function
|
||||||
// Attempt to find the LoadRelationshipName function
|
loadMethod, found := r.Type.MethodByName(loadMethodPrefix + current)
|
||||||
loadMethod, found := rel.Type.MethodByName("Load" + relationship)
|
if !found {
|
||||||
if !found {
|
return errors.Errorf("could not find %s%s method for eager loading", loadMethodPrefix, current)
|
||||||
return errors.Errorf("could not find Load%s method for eager loading", relationship)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
execArg := reflect.ValueOf(q.executor)
|
// Hack to allow nil executors
|
||||||
if !execArg.IsValid() {
|
execArg := reflect.ValueOf(exec)
|
||||||
execArg = reflect.ValueOf((*sql.DB)(nil))
|
if !execArg.IsValid() {
|
||||||
}
|
execArg = reflect.ValueOf((*sql.DB)(nil))
|
||||||
|
}
|
||||||
|
|
||||||
methodArgs := []reflect.Value{
|
methodArgs := []reflect.Value{
|
||||||
reflect.Indirect(reflect.New(rel.Type)),
|
reflect.Indirect(reflect.New(r.Type)),
|
||||||
execArg,
|
execArg,
|
||||||
reflect.ValueOf(singular),
|
reflect.ValueOf(singular),
|
||||||
reflect.ValueOf(obj),
|
reflect.ValueOf(obj),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := loadMethod.Func.Call(methodArgs)
|
resp := loadMethod.Func.Call(methodArgs)
|
||||||
if resp[0].Interface() != nil {
|
if resp[0].Interface() != nil {
|
||||||
return resp[0].Interface().(error)
|
return errors.Wrapf(resp[0].Interface().(error), "failed to eager load %s", current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull one off the queue, continue if there's still some to go
|
||||||
|
toLoad = toLoad[1:]
|
||||||
|
if len(toLoad) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedObject := reflect.ValueOf(obj)
|
||||||
|
// If we eagerly loaded nothing
|
||||||
|
if loadedObject.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
loadedObject = loadedObject.Elem()
|
||||||
|
|
||||||
|
// If it's singular we can just immediately call without looping
|
||||||
|
if singular {
|
||||||
|
return loadRelationshipsRecurse(exec, current, toLoad, singular, loadedObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all eager loaded objects
|
||||||
|
ln := loadedObject.Len()
|
||||||
|
if ln == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := 0; i < ln; i++ {
|
||||||
|
iter := loadedObject.Index(i).Elem()
|
||||||
|
if err := loadRelationshipsRecurse(exec, current, toLoad, singular, iter); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadRelationshipsRecurse is a helper function for taking a reflect.Value and
|
||||||
|
// Basically calls loadRelationships with: obj.R.EagerLoadedObj, and whether it's a string or slice
|
||||||
|
func loadRelationshipsRecurse(exec Executor, current string, toLoad []string, singular bool, obj reflect.Value) error {
|
||||||
|
r := obj.FieldByName(relationshipStructName)
|
||||||
|
if !r.IsValid() || r.IsNil() {
|
||||||
|
return errors.Errorf("could not traverse into loaded %s relationship to load more things", current)
|
||||||
|
}
|
||||||
|
newObj := r.Elem().FieldByName(current)
|
||||||
|
singular = newObj.Elem().Kind() == reflect.Struct
|
||||||
|
return loadRelationships(exec, toLoad, newObj.Interface(), singular)
|
||||||
|
}
|
||||||
|
|
||||||
// bindChecks resolves information about the bind target, and errors if it's not an object
|
// bindChecks resolves information about the bind target, and errors if it's not an object
|
||||||
// we can bind to.
|
// we can bind to.
|
||||||
func bindChecks(obj interface{}) (structType reflect.Type, sliceType reflect.Type, singular bool, err error) {
|
func bindChecks(obj interface{}) (structType reflect.Type, sliceType reflect.Type, singular bool, err error) {
|
||||||
|
|
|
@ -165,25 +165,74 @@ func TestBindSingular(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadFunctionCalled bool
|
var loadFunctionCalled bool
|
||||||
|
var loadFunctionNestedCalled int
|
||||||
|
|
||||||
type testRStruct struct{}
|
type testRStruct struct {
|
||||||
|
}
|
||||||
|
type testNestedStruct struct {
|
||||||
|
ID int
|
||||||
|
R *testNestedRStruct
|
||||||
|
}
|
||||||
|
type testNestedRStruct struct {
|
||||||
|
ToEagerLoad *testNestedStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
type testNestedSlice struct {
|
||||||
|
ID int
|
||||||
|
R *testNestedRSlice
|
||||||
|
}
|
||||||
|
type testNestedRSlice struct {
|
||||||
|
ToEagerLoad *[]*testNestedSlice
|
||||||
|
}
|
||||||
|
|
||||||
func (r *testRStruct) LoadTestOne(exec Executor, singular bool, obj interface{}) error {
|
func (r *testRStruct) LoadTestOne(exec Executor, singular bool, obj interface{}) error {
|
||||||
loadFunctionCalled = true
|
loadFunctionCalled = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *testNestedRStruct) LoadToEagerLoad(exec Executor, singular bool, obj interface{}) error {
|
||||||
|
switch x := obj.(type) {
|
||||||
|
case *testNestedStruct:
|
||||||
|
x.R = &testNestedRStruct{
|
||||||
|
&testNestedStruct{ID: 5},
|
||||||
|
}
|
||||||
|
case *[]*testNestedStruct:
|
||||||
|
for _, r := range *x {
|
||||||
|
r.R = &testNestedRStruct{
|
||||||
|
&testNestedStruct{ID: 5},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *testNestedRSlice) LoadToEagerLoad(exec Executor, singular bool, obj interface{}) error {
|
||||||
|
|
||||||
|
switch x := obj.(type) {
|
||||||
|
case *testNestedSlice:
|
||||||
|
newSlice := []*testNestedSlice{&testNestedSlice{ID: 5}}
|
||||||
|
x.R = &testNestedRSlice{&newSlice}
|
||||||
|
case *[]*testNestedSlice:
|
||||||
|
newSlice := []*testNestedSlice{&testNestedSlice{ID: 5}}
|
||||||
|
for _, r := range *x {
|
||||||
|
r.R = &testNestedRSlice{&newSlice}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadRelationshipsSlice(t *testing.T) {
|
func TestLoadRelationshipsSlice(t *testing.T) {
|
||||||
// t.Parallel() Function uses globals
|
// t.Parallel() Function uses globals
|
||||||
loadFunctionCalled = false
|
loadFunctionCalled = false
|
||||||
|
|
||||||
testSlice := []*struct {
|
testSlice := []*struct {
|
||||||
ID int
|
ID int
|
||||||
R *testRStruct
|
R *testRStruct
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
q := Query{load: []string{"TestOne"}, executor: nil}
|
if err := loadRelationships(nil, []string{"TestOne"}, &testSlice, false); err != nil {
|
||||||
if err := q.loadRelationships(&testSlice, false); err != nil {
|
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,12 +246,11 @@ func TestLoadRelationshipsSingular(t *testing.T) {
|
||||||
loadFunctionCalled = false
|
loadFunctionCalled = false
|
||||||
|
|
||||||
testSingular := struct {
|
testSingular := struct {
|
||||||
ID int
|
ID int
|
||||||
R *testRStruct
|
R *testRStruct
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
q := Query{load: []string{"TestOne"}, executor: nil}
|
if err := loadRelationships(nil, []string{"TestOne"}, &testSingular, true); err != nil {
|
||||||
if err := q.loadRelationships(&testSingular, true); err != nil {
|
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,6 +259,60 @@ func TestLoadRelationshipsSingular(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadRelationshipsSliceNested(t *testing.T) {
|
||||||
|
// t.Parallel() Function uses globals
|
||||||
|
testSlice := []*testNestedStruct{
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled = 0
|
||||||
|
if err := loadRelationships(nil, []string{"ToEagerLoad", "ToEagerLoad", "ToEagerLoad"}, &testSlice, false); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if loadFunctionNestedCalled != 3 {
|
||||||
|
t.Error("Load function was called:", loadFunctionNestedCalled, "times")
|
||||||
|
}
|
||||||
|
|
||||||
|
testSliceSlice := []*testNestedSlice{
|
||||||
|
{
|
||||||
|
ID: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled = 0
|
||||||
|
if err := loadRelationships(nil, []string{"ToEagerLoad", "ToEagerLoad", "ToEagerLoad"}, &testSliceSlice, false); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if loadFunctionNestedCalled != 3 {
|
||||||
|
t.Error("Load function was called:", loadFunctionNestedCalled, "times")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadRelationshipsSingularNested(t *testing.T) {
|
||||||
|
// t.Parallel() Function uses globals
|
||||||
|
testSingular := testNestedStruct{
|
||||||
|
ID: 5,
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled = 0
|
||||||
|
if err := loadRelationships(nil, []string{"ToEagerLoad", "ToEagerLoad", "ToEagerLoad"}, &testSingular, true); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if loadFunctionNestedCalled != 3 {
|
||||||
|
t.Error("Load function was called:", loadFunctionNestedCalled, "times")
|
||||||
|
}
|
||||||
|
|
||||||
|
testSingularSlice := testNestedSlice{
|
||||||
|
ID: 5,
|
||||||
|
}
|
||||||
|
loadFunctionNestedCalled = 0
|
||||||
|
if err := loadRelationships(nil, []string{"ToEagerLoad", "ToEagerLoad", "ToEagerLoad"}, &testSingularSlice, true); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if loadFunctionNestedCalled != 3 {
|
||||||
|
t.Error("Load function was called:", loadFunctionNestedCalled, "times")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBind_InnerJoin(t *testing.T) {
|
func TestBind_InnerJoin(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue