Imagining that I have a N-N relationship between Student and the Course, .NET structure is typical N-N translated to CLR types Student, StudentCourse and Course.
Desired result:
What I want to achieve when querying for all students like this ...
query {
allStudents {
name,
courses {
name,
rating
}
}
}
... Is to avoid N+1 graph issues.
Instead I would like to end end up with the query similar to this one:
SELECT s.Name
FROM [Schema].[Student] AS [s]
LEFT JOIN (
SELECT course_id, student_id
FROM [Relationships].[student_course] AS [c]
INNER JOIN [Schema].[course] AS [c0] ON [c].[course_id] = [c0].[id]
) AS [t] ON [s].[id] = [t].[student_id]
WHERE [m].[id] IN (1, 2, 4, 8, 10, 12) -- All student id's are loaded with data loader?
ORDER BY [m].[id], [t].[id]
Current implementation glimpse:
I created a resolver GetCoursesForStudent for batch fetching course for student.
public async Task<Course> GetCoursesForStudent([Parent] Student student,
CoursesForStudentDataLoader dataLoader,
IResolverContext context,
CancellationToken cancellationToken = default!)
{
// This is generating a SELECT for each student, maybe load in a batch somehow?
var courseIds = await service.GetCourseIdsForStudent(student.Id, cancellationToken);
return await dataLoader.LoadAsync(student.Id, cancellationToken);
}
And the data loader
public class CoursesForStudentDataLoader : BatchDataLoader<int, Course>
{
public CoursesForStudentDataLoader(IBatchScheduler batchScheduler)
: base(batchScheduler)
{
}
protected override async Task<IReadOnlyDictionary<int, Course>> LoadBatchAsync(IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
var courses = repo.Queryable()
.Where(x => keys.Contains(x.StudentId))
.Select(item => item.Course);
var fetched = await courses.ToDictionaryAsync(key => key.Id, cancellationToken);
// fetched contains all the courses!
return fetched;
}
}
And object type definition with the resolver for companies.
protected override void Configure(IObjectTypeDescriptor<Student> descriptor)
{
descriptor.Field<StudentResolvers>(t =>
t.GetCoursesForStudent(default!, default!, default!, default!)).Name("courses")
.Type<ListType<NonNullType<CourseObjectType>>>();
}
Current results:
Currently this generates a sql query for each student to fetch the course id's for each student. (N+1 issue) And then it uses a data loader to buffer course ids and executes the query to fetch the courses only once. (N+1 avoided, and this looks good).
So I end up with the following sql:
SELECT [c].[course_id]
FROM [Relationships].[student_course] AS [c]
WHERE [c].[student_id] = @__studentId_0
SELECT [c].[course_id]
FROM [Relationships].[student_course] AS [c]
WHERE [c].[student_id] = @__studentId_0
SELECT [c].[course_id]
FROM [Relationships].[student_course] AS [c]
WHERE [c].[student_id] = @__studentId_0
... - N (clearly undesirable)
SELECT [c].[id], [c].[name], [c].[rating]
FROM [Schema].[course] AS [c]
WHERE [c].[id] IN (7, 9, 12)
So I wonder what is the way to go with this, is it even possible or I am misinterpreting something.