EF Core与GraphQL

20

我目前正在探索GraphQL开发,正在探索EF Core生成哪种SQL查询语句。我注意到,即使我的GraphQL查询只包括几个字段,EF Core也会为实体的所有字段发送SQL Select。

这是我现在使用的代码:

public class DoctorType : ObjectGraphType<Doctors>
{
    public DoctorType()
    {
            Field(d => d.PrefixTitle);
            Field(d => d.FName);
            Field(d => d.MName);
            Field(d => d.LName);
            Field(d => d.SufixTitle);
            Field(d => d.Image);
            Field(d => d.EGN);
            Field(d => d.Description);
            Field(d => d.UID_Code); 
    }
}

public class Doctors : ApplicationUser
{
    public string Image { get; set; }
    [StringLength(50)]
    public string UID_Code { get; set; }
}

我使用的查询语句是

{
  doctors{
    fName
    lName
  }
}

生成的SQL选择Doctor实体的所有字段。
有没有进一步优化EF Core生成的SQL查询的方法?
我猜这是因为DoctorType继承自>而不是Doctor的某些Projection,但我想不出聪明的解决办法?
有什么建议吗?
编辑:
我正在使用Joe McBride版本2.4.0的GraphQL.NET(graphql-dotnet)。
编辑2:
我可能做错了或者不知道该怎么做。
正如其中一个评论所建议的那样,我下载了SimonCropp的GraphQL.EntityFramework Nuget包。
我做了所有必要的配置:
        services.AddDbContext<ScheduleDbContext>(options =>
        {
            options.UseMySql(Configuration.GetConnectionString("DefaultConnection"));
        });

        using (var myDataContext = new ScheduleDbContext())
        {
            EfGraphQLConventions.RegisterInContainer(services, myDataContext);
        }

我的对象图类型如下所示

public class SpecializationType : EfObjectGraphType<Specializations>
{
    public SpecializationType(IEfGraphQLService graphQlService)
        :base(graphQlService)
    {
        Field(p => p.SpecializationId);
        Field(p => p.Code);
        Field(p => p.SpecializationName);
    }
}

我的查询如下:

public class RootQuery : EfObjectGraphType
{
    public RootQuery(IEfGraphQLService efGraphQlService,
        ScheduleDbContext dbContext) : base(efGraphQlService)
    {
        Name = "Query";

        AddQueryField<SpecializationType, Specializations>("specializationsQueryable", resolve: ctx => dbContext.Specializations);

    }
}

我正在使用这个GraphQL查询

{
  specializationsQueryable
  {
    specializationName
  }
}

调试日志显示生成的SQL查询语句为:
SELECT `s`.`SpecializationId`, `s`.`Code`, `s`.`SpecializationName`
FROM `Specializations` AS `s`

尽管我想要的只是 specializationName 字段,我期望它是这样的:
SELECT `s`.`SpecializationName`
FROM `Specializations` AS `s`

更新

我认为到目前为止我还没有真正理解GraphQL的工作方式。我以为有一些在幕后获取数据的过程,但实际上没有。

主要的获取过程是在查询字段解析器中完成的:

FieldAsync<ListGraphType<DoctorType>>("doctors", resolve: async ctx => await doctorServices.ListAsync());

只要在我的情况下解析器返回Doctors实体的列表,其结果是完整的对象,它将查询整个实体(所有字段)的数据库。无论您返回IQueryable还是其他实体,GraphQL默认不执行任何优化。
这里的每个结论都是我考虑出来的,不能保证100%正确。
因此,我创建了一组Helper方法,用于创建选择Expression以在LINQ查询中使用。助手使用解析器的context.SubFields属性获取所需字段。
问题在于你只需要在每个查询级别的叶子上说一些查询“specializations”与“SpecializationName”和“Code”,以及他们的“Name”和其他医生。在这种情况下,在RootQuery专业领域的解析器中,您只需要对Specializations实体进行投影:SpecializationNameCode,然后当它进入从SpecializationType中的“doctors”字段中提取所有Doctors时,解析器的上下文具有应用于Doctor的投影所需的不同SubFields。
上述问题是,即使您没有使用查询批处理,我想SpecializationType中的Doctors字段仍然需要在RootQuery专业字段中获取SpecializationId。 在专业类型中。
我觉得我没有很好地解释我经历了什么。
基线是据我所知,我们必须动态创建选择器,linq应该用它来投影实体。
我在这里发布我的方法:
    public class RootQuery : EfObjectGraphType
{
    public RootQuery(IEfGraphQLService efGraphQlService, ISpecializationGraphQlServices specializationServices,
        IDoctorGraphQlServices doctorServices, ScheduleDbContext dbContext) : base(efGraphQlService)
    {
        Name = "Query";

        FieldAsync<ListGraphType<SpecializationType>>("specializations"
            , resolve: async ctx => {

                var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
                var expression = BuildLinqSelectorObject.DynamicSelectGenerator<Specializations>(selectedFields.ToArray());

                return await specializationServices.ListAsync(selector: expression);
            });
    }
}

专业类型

 public class SpecializationType : EfObjectGraphType<Specializations>
{
    public SpecializationType(IEfGraphQLService graphQlService
        , IDataLoaderContextAccessor accessor, IDoctorGraphQlServices doctorServices)
        : base(graphQlService)
    {
        Field(p => p.SpecializationId);
        Field(p => p.Code);
        Field(p => p.SpecializationName);
        Field<ListGraphType<DoctorType>, IEnumerable<Doctors>>()
            .Name("doctors")
            .ResolveAsync(ctx =>
            {

                var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
                selectedFields = GraphQLResolverContextHelpers.AppendParrentNodeToEachItem(selectedFields, parentNode: "Doctor");
                selectedFields = selectedFields.Union(new[] { "Specializations_SpecializationId" });

                var expression = BuildLinqSelectorObject.BuildSelector<SpecializationsDoctors, SpecializationsDoctors>(selectedFields);

                var doctorsLoader = accessor.Context
                    .GetOrAddCollectionBatchLoader<int, Doctors>(
                        "GetDoctorsBySpecializationId"
                        , (collection, token) =>
                        {
                            return doctorServices.GetDoctorsBySpecializationIdAsync(collection, token, expression);
                        });
                return doctorsLoader.LoadAsync(ctx.Source.SpecializationId);
            });
    }
}

医生服务:

public class DoctorGraphQlServices : IDoctorGraphQlServices
{
    public ScheduleDbContext _dbContext { get; set; }

    public DoctorGraphQlServices(ScheduleDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<List<Doctors>> ListAsync(int? specializationId = null)
    {
        var doctors = _dbContext.Doctors.AsQueryable();

        if(specializationId != null)
        {
            doctors = doctors.Where(d => d.Specializations.Any(s => s.Specializations_SpecializationId == specializationId));
        }

        return await doctors.ToListAsync();
    }

    public async Task<ILookup<int, Doctors>> GetDoctorsBySpecializationIdAsync(IEnumerable<int> specializationIds, CancellationToken token, Expression<Func<SpecializationsDoctors, SpecializationsDoctors>> selector = null)
    {
        var doctors = await _dbContext.SpecializationsDoctors
            .Include(s => s.Doctor)
            .Where(spDocs => specializationIds.Any(sp => sp == spDocs.Specializations_SpecializationId))
            .Select(selector: selector)
            .ToListAsync();

        return doctors.ToLookup(i => i.Specializations_SpecializationId, i => i.Doctor);
    }

}

专业化服务

public class SpeciaizationGraphQlServices : ISpecializationGraphQlServices
{

    public ScheduleDbContext _dbContext { get; set; }

    public SpeciaizationGraphQlServices(ScheduleDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<dynamic> ListAsync(string doctorId = null, Expression<Func<Specializations, Specializations>> selector = null)
    {
        var specializations = _dbContext.Specializations.AsQueryable();

        if (!string.IsNullOrEmpty(doctorId))
        {
            specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
        }

        return await specializations.Select(selector).ToListAsync();

    }

    public async Task<ILookup<string, Specializations>> GetSpecializationsByDoctorIdAsync(IEnumerable<string> doctorIds, CancellationToken token)
    {
        var specializations = await _dbContext.SpecializationsDoctors
            .Include(s => s.Specialization)
            .Where(spDocs => doctorIds.Any(sp => sp == spDocs.Doctors_Id))
            .ToListAsync();

        return specializations.ToLookup(i => i.Doctors_Id, i => i.Specialization);
    }

    public IQueryable<Specializations> List(string doctorId = null)
    {
        var specializations = _dbContext.Specializations.AsQueryable();

        if (!string.IsNullOrEmpty(doctorId))
        {
            specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
        }

        return specializations;
    }
}

这篇文章已经变得相当长了,很抱歉跨度如此之大。


EF Core的配置和映射是什么样子的?如何加载数据? - Panagiotis Kanavos
1
你正在使用哪个GraphQL库?GraphQL.NET是一个常见的选择,但并不是唯一的选择。如果没有GraphQL.EntityFramework,就无法直接映射到EF。 - Panagiotis Kanavos
你的意思是GraphQL.EntityFramework会处理生成与GraphQL查询请求的字段匹配的SQL SELECT查询? - Stefan PEev
我会阅读GraphQL.EntityFramework文档。感谢您指引前进的道路。 - Stefan PEev
5个回答

3
在.NET Conf 2021上,@jeremylikness进行了一场关于GraphQL和EF Core 6的讲座。我建议使用.NET 6并查看他的讲话:

https://devblogs.microsoft.com/dotnet/get-to-know-ef-core-6/#graphql

https://aka.ms/graphql-efcore

https://www.youtube.com/watch?v=GBvTRcV4PVA

https://www.youtube.com/watch?v=4nqjB_z5CU0

这是一个使用Hot Chocolate GraphQL服务器的示例实现:

https://chillicream.com/docs/hotchocolate/integrations/entity-framework

这是微软在其高级计划中关于EF Core 6.0的GraphQL写的内容:
“在过去几年中,GraphQL已经在各种平台上获得了越来越多的关注。我们计划研究这个领域,并找到改进.NET体验的方法。这将涉及与社区合作,了解和支持现有的生态系统。这也可能涉及到微软的具体投资,无论是对现有工作的贡献还是在Microsoft堆栈中开发互补的部分。”

https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/plan#graphql


1

为了启用自动字段投影,在通常的.NET 6设置下,添加hotchocolate服务器:

dotnet add package HotChocolate.Data.EntityFramework

将一些数据公开给它:

public class MyQueries
{
    [UseProjection] // Enables field projection
    public IQueryable<Book> Books([Service] MyContext db) => db.Books;
}

Program.cs 中启用它:

builder.Services.AddGraphQLServer().AddQueryType<MyQueries>().AddProjections();
...
app.MapGraphQL("/graphql");

这应该足以确保自动数据库字段投影。现在,您可以通过生成器在/graphql/上运行GraphQL查询,同时通过MyContext.Database.Log = Console.Write;监视SQL。


1
我建议您:
1-使用dto模型并将其映射到数据库模型
这意味着您需要将输入的dto模型转换为数据库模型以保存在数据库中;还需要将从实体框架数据库选择中获取的数据库模型转换为dto模型。
这是制作通用api时使用的经典方法,例如获取输入请求中的dto模型数据,将dto转换为保存在数据库中的数据,反之亦然。
2-将dto模型映射到graphqltypes(objectgraphtype和inputobjectgraphtype)
这意味着对于每个dto模型,可能需要编写1个objectgraphtype和1个inputobjectgraphtype。
为了做到这一点,我创建了一个自动dto到graphtype转换器,因此您不需要编写K和K的代码!(请参见末尾的链接)
3-不要使用AddDbContext! Graphql中间件使用单例模式;通过依赖注入在graphql中使用的所有内容都是外部单例,即使它被注册为scoped(AddDbContext表示“scoped”)。
这意味着您在启动时只有1个连接。您不能同时执行2个数据库操作!
在现实生活中,您不能在Graphql中使用AddDbContext!
您可以使用工厂模式来实现这一点。因此,在依赖注入中不要传递dbcontext,而是传递一个Func并显式地实例化dbcontext。
以下是完整的实现示例: https://github.com/graphql-dotnet/graphql-dotnet/issues/576#issuecomment-626661695

1

对于DoctorType,请检查定义的ObjectGraphType,该类型用于返回Doctors

例如,我有以下PlayerType

public class PlayerType : ObjectGraphType<Player>
{
    public PlayerType(ISkaterStatisticRepository skaterStatisticRepository)
    {
        Field(x => x.Id);
        Field(x => x.Name, true);
        Field(x => x.BirthPlace);
        Field(x => x.Height);
        Field(x => x.WeightLbs);
        Field<StringGraphType>("birthDate", resolve: context => context.Source.BirthDate.ToShortDateString());
        Field<ListGraphType<SkaterStatisticType>>("skaterSeasonStats",
            arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
            resolve: context => skaterStatisticRepository.Get(context.Source.Id), description: "Player's skater stats");
    }
}

我返回 Field<ListGraphType<PlayerType>> 通过

public class NHLStatsQuery : ObjectGraphType
{
    public NHLStatsQuery(IPlayerRepository playerRepository, NHLStatsContext dbContext)
    {
        Field<ListGraphType<PlayerType>>(
            "players",
            resolve: context => {
                return dbContext.Players.Select(p =>new Player { Id = p.Id, Name = p.Name });
                //return playerRepository.All();
            });
    }
}

查询及其列由字段中的resolve控制。

无论您想返回哪些字段,请确保在resolve中返回PlayerType中定义的列。


这段代码明确请求EF查询中的“Id,Name”列。它不使用GraphQL查询来决定返回哪些字段。 - Panagiotis Kanavos
@PanagiotisKanavos 你说得对。我使用这种方式来控制 NHLStatsQuery 生成的查询。有没有更好的方法来控制生成的 GraphQL 查询呢? - Edward

0
我正在使用Joe McBride版本2.4.0的GraphQL.NET(graphql-dotnet)。
首先,我建议至少更新到v4.6-有很多修复和有用的更新。
其次,如果您没有数据突变(即-更新/删除/插入数据),我会说最好不要使用EF来获取数据。基于相同的GraphQL.Net库,例如,您可以查看NReco.GraphQL,它使用轻量级ORM来获取和映射数据(您只需要在json文件中定义模式)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接