ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.24k stars 744 forks source link

[Proof of Concept] Faster database queries and less boilerplate with automatic entity navigation #2460

Closed zaneclaes closed 2 years ago

zaneclaes commented 4 years ago

Motivation

As I went through the tutorials, I couldn't help but notice some inefficiencies in the DataLoader implementation(s), which are over-fetching by an order of magnitude or so. It also seemed like there was an opportunity to reduce boilerplate code.

I have created a working, generic solution to this problem using EFCore5's navigation properties.

Improvements

To take the first DataLoader example (Speakers -> Sessions)...

This latter point is a classic N+1 query. Avoiding N+1 queries is pretty much the whole point of using a data loader.

Syntax (Example)

With this proof-of-concept, it is assumed your models follow standard entity relationships. I have focused on many-to-many relationships in EFCore5 with implicit tables to demonstrate this feature.

First, for convenience, create a concrete extension for your application primary key type and DbContext type (string and OWSData in my case):

namespace OpenWorkShop.Data {
  public static class OWSEntityResolverExtensions {
    public static IObjectFieldDescriptor Entity<TData, TProp>(
      this IObjectTypeDescriptor<TData> desc, Expression<Func<TData, ICollection<TProp>>> propertyOrMethod
    ) where TData : IModelId<string> where TProp : class, IModelId<string> =>
      desc.Entity<string, TData, TProp, OWSData>(propertyOrMethod);
  }
}

Then, to set up the many-to-many relationship, just use:

descriptor.Entity(t => t.Sessions).Name("sessions");

That's it.

You delete any code for:

Under the Hood

Instead of implementing the Resolver by querying the Speaker table for a second time (and including the SessionSpeakers), this extension insects the navigation properties on both classes. Then, it directly queries the intermediate table (without any joins) to discover the involved sessions. Finally, it queries the sessions table to return the results.

The total number of database queries should always equal the number of tables involved (four, in this case).

Testing

I'm using this with 6 different classes, each of which have a variety of many-to-many relationship. With the extension set up, it's just one line of code per relationship (descriptor.Entity()). So far, it's all working flawlessly.

n.b. I ported this from work I did with the GraphQL library, so I had already tested a fair bit of the code.

Limitations

Source Code

https://github.com/zaneclaes/hot-chocolate-entity-framework-resolver

michaelstaib commented 4 years ago

If you do an initial pr I can add compiler support and attribute support so that it works with fluent but also with attributes.

michaelstaib commented 4 years ago

When you clone the repo, run:

./build.sh restore

Which will restore the packages and generate a new solution called All.sln located in src.

If you are using vscode like myself, you can also use the scoped solutions... so basically, head over to data and use the HotChocolate.Data.sln instead.

EF specific code goes here: https://github.com/ChilliCream/hotchocolate/tree/develop/src/HotChocolate/Data/src/EntityFramework

zaneclaes commented 4 years ago

Great — CI tool ran, and project is building. Took a while because I'm on DSL in rural Colorado ;)

zaneclaes commented 4 years ago

@michaelstaib do you have a recommendation how to ditch the IModelId<TKey> requirement? I think what I want is a way to access the INodeDescriptor<TNode>. Given only the TNode, I need a way to determine the TId, as well as retrieve its value from a TNode instance.

michaelstaib commented 4 years ago

Yes, with a lazy config .... we will hook in and wait until the init phase is completed and then grab the node member. But we could even solve this more generically by resolving the Key member whenever we do not find node.

zaneclaes commented 4 years ago

Is there an existing mechanism I can call so that this is included in my PR, or is this something you intend to build?

michaelstaib commented 4 years ago

All is in place ...

There are various ways to do this:

So, the easiest way to I think is like the following:

descriptor.Extend().OnBeforeNaming((c, d) =>
{
    ....
})

The type system is initialized in three phases....

  1. everything is being registered.
  2. types are being named
  3. types are being completed.

So,

we will hook in just right before 2 and then just grab the member from the node field. If the type has no node field we will just look the key prop up.

d.Fields.TryGet("id" ....

We could tell the node descriptor to put a node member on the context to make this even easier....

can you create a PR and I can put some stubs in for this?

zaneclaes commented 4 years ago

Sure, if that's easier for you.

As I get it up, one other related question:

What is the idiomatic HotChocolate way to access the user-configured/scoped DbContext instance (should I just GetRequiredService<DbContext> or otherwise DI the generic type)? I don't want to make the user re-type the DB context; it inflates the number of generics required to be passed.

michaelstaib commented 4 years ago

The best way to access the dbcontext is through the new IDbContextFactory. This allows the execution engine to fetch in parallel. On the configuration side, it should be configured to use pooling so that we do not have too many allocations around creating it and dropping it.

zaneclaes commented 4 years ago

Yeah, I'm using that, but what I'm trying to ask is about getting rid of the <TDbContext> requirement. It seems IDbContextFactory requires typing, which I do not wish to provide — so as to avoid additional generic typing on the behalf of the end-user.

Specifically, IDbContextFactory<DbContext> fails to resolve.

michaelstaib commented 4 years ago

I will have a look at your code again later ... but yes there are ways to store it on the context ... we do that with a lot of other things.

nguyenquyhy commented 3 years ago

Is there any update on this or any new recommendation on using Entity Framework? I can't find much information on using HotChocolate with Entity Framework in the v11 docs.

Right now any navigation properties of Entity Framework will return null, or Cannot access a disposed context instance. if lazy proxies is used. So I have to write individual field descriptor for each navigation to overwrite EF navigation property. Am I doing it wrong and is there a better way to use EF than this?

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.