graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

Relation Efficiency Solutions

Open jacobaclarke opened this issue 3 years ago • 8 comments

I am trying to make a very large request that does lookups on several different collections to build a large graph. This ends up being very slow because the data has to be sent between my server and database several times to make the request.

It is pretty simple to add lookups to an object in mql and save the combined graph as a view. However, I can't access the combined types from graphql-compose-mongoose. Is there any way to create views from graphql-compose-mongoose that I can then use as type composers?

jacobaclarke avatar Apr 02 '21 01:04 jacobaclarke

Here is a simple example of what I would like to do:

Models:

users {
	eventIds
}
events {
	name
	extraRandomFieldIDontWantProjected
}

Then I would create a view from this aggregation

Users.aggregate[
{ $lookup:
	  {
	    from: 'events',
	    localField: 'eventIds',
	    foreignField: '_id',
	    as: 'events'
	  }

}
]

Then I could make a graphql request like this on the newly created view

users {
	events {
		name
	}
}

I understand that this same functionality exists on addRelation, but if I understand correctly, the addRelation approach requires multiple requests to the datbase, whereas this would require one.

Thanks a ton in advance. I really appreciate any input you have.

jacobaclarke avatar Apr 02 '21 01:04 jacobaclarke

It looks like your implementation of dataloader may do this, but I am having a hard time understanding it. From my understanding, it caches the _id index from the last request so that repeated lookups don't need to make a trip to the database. My use case is below:

Resolvers.StudentTC.addRelation("clinics", {
  resolver: Resolvers.ClinicTC.mongooseResolvers.findMany(),
  prepareArgs: {
    filter: (source) => ({
      isActive: true,
      positions: { signUps: { studentId: source["_id"] } },
    }),
  },
  projection: { ["_id"]: 1 },
} as any);

This doesn't seem to work with the implementation because studentId on the foreign document is a nested subdocument and dataLoader requires that the foreignKey be on the main document. Am I understanding this right?

Sorry for all the questions. This repo is a great tool!

jacobaclarke avatar Apr 02 '21 12:04 jacobaclarke

@j718 you have a complex issue that is not covered by graphql-compose-mongoose code (resolver) generators. And frankly, it really quite complex or maybe impossible to build the out-of-the-box solution.

My recommendation is to write your complex resolvers manually. You should write some Service or DataManager which will fetch data from database in performance way via aggregation or any other way. And after that from your manual written resolvers just call your Service (DataManager).

UserTC.addField({
  events: {
    type: EventTC.List,
    resolve: (userData, args, context, info) => {
      return yourEventServiceWithCustomDataLoaderImplementationForNestedField.loadMany(userData.eventIds);
    }
  }
})

nodkz avatar Apr 05 '21 08:04 nodkz

Thanks for the feedback. I'll try that out.

jacobaclarke avatar Apr 05 '21 23:04 jacobaclarke

@nodkz Is there any difference in efficiency between adding a relation through a mongoose virtual vs the TC.addRelation method?

jacobaclarke avatar Apr 18 '21 20:04 jacobaclarke

addRelation has a small runtime overhead in args preparation https://github.com/graphql-compose/graphql-compose/blob/e7467d505c765beeec6d53f17a608270545daef6/src/ObjectTypeComposer.js#L1783-L1798

But it so small that we can neglect it.

nodkz avatar Apr 22 '21 15:04 nodkz

Perhaps, you need to write your custom dataloader in order to batch requests, if so I wrote an example here which uses graphql-compose-mongoose

yossisp avatar May 01 '21 14:05 yossisp

Looks like a great post. Thanks for sharing! I'll try implementing this

jacobaclarke avatar May 01 '21 21:05 jacobaclarke