EF Core с GraphQL

В настоящее время я изучаю разработку GraphQL и в настоящее время изучаю, какие типы запросов SQL генерируются через EF Core, и я заметил, что независимо от того, что мой запрос 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 выбирает все поля объекта "Доктор".

Есть ли способ дальнейшей оптимизации этого сгенерированного SQL-запроса из EF Core?

Я предполагаю, что это происходит потому, что DoctorType наследует от ObjectGraphType<Doctors> а не из какой-то проекции доктора, но я не могу придумать умного обходного пути?

Какие-либо предложения?

РЕДАКТИРОВАТЬ:

Я использую GraphQL.NET (graphql-dotnet) от Joe McBride версии 2.4.0

РЕДАКТИРОВАТЬ 2:

Либо я делаю это неправильно, либо я не знаю.

В качестве одного из предложенных комментариев я скачал пакет GraphQL.EntityFramework Nuget от SimonCropp

Я сделал всю конфигурацию, необходимую для этого:

        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 объект, он будет запрашивать базу данных для всего объекта (все поля). Оптимизация из GraphQL из коробки не производится, не имеет значения, возвращаете ли вы IQueryable или запрашиваемую вами сущность.

Каждый вывод здесь мой, это не 100% право

Поэтому я создал группу вспомогательных методов, которые создают выражение выбора для использования в запросе LINQ. Помощники используют свойство resolver context.SubFields для получения необходимых полей.

Проблема в том, что для каждого уровня запроса вам нужны только листья, скажем, некоторые "специализации" запроса с "SpecializationName" и "Code" и "Doctors" с их "Name" и другие. В этом случае в RootQuery распознаватель поля специализаций вам нужно только Specializations проекция объекта так: SpecializationName а также Code тогда, когда он идет, чтобы получить все Doctors с поля "врачи" в SpecializationType контекст резолвера имеет разные подполя, которые должны использоваться для проекции Doctor,

Проблема с вышесказанным заключается в том, что когда вы используете пакеты запросов, я думаю, даже если вы не понимаете, что Doctors Поле в SpecializationType нужен идентификатор специализации, извлеченный в RootQuery Область специализации.

Думаю, я не очень хорошо объяснил, через что прошел.

Насколько я понимаю, базовая линия состоит в том, что мы должны динамически создавать селекторы, которые 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);
            });
    }
}

SpecializationType

 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);
            });
    }
}

DoctorsServices:

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);
    }

}

SpecializationServices

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;
    }
}

Этот пост стал довольно большим, извините за промежуток..

5 ответов

@jeremylikness рассказал о GraphQL с EF Core 6 на конференции .NET 2021. Я бы рекомендовал использовать .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

Вот что Microsoft написала о GraphQL для EF Core 6.0 в своем плане высокого уровня:

За последние несколько лет GraphQL набирает обороты на различных платформах.Мы планируем изучить пространство и найти способы улучшить работу с .NET. Это будет включать работу с сообществом над пониманием и поддержкой существующей экосистемы. Это может также потребовать определенных инвестиций со стороны Microsoft либо в виде вклада в существующую работу, либо в разработку дополнительных элементов в стеке Microsoft.

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

Чтобы включить автоматическую проекцию полей, настроив контекст БД как обычно для .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/, при мониторинге SQL через MyContext.Database.Log = Console.Write;

За 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 в поле.

Независимо от того, какие поля вы хотите вернуть, убедитесь, что столбцы, определенные в PlayerType возвращаются в resolve,

Я предлагаю тебе:

1-используйте модели dto и сопоставьте их с моделями базы данных

Это означает, что вам нужно преобразовать входную модель dto в модель базы данных для сохранения в db; а также конвертировать модели базы данных, полученные из выбора базы данных Entity Framework, в модель dto.

Это классический подход, используемый при создании универсального API, который, например, получает данные модели dto во входном запросе, конвертирует dto для сохранения данных в базе данных и наоборот.

2-карта dto-модели в graphqltypes (objectgraphtype и inputobjectgraphtype)

Это означает, что для каждой модели dto может потребоваться написать 1 тип графа объекта и 1 тип впутобъекта.

ДЛЯ ЭТОГО Я СОЗДАЮ АВТОМАТИЧЕСКИЙ DTO В ПРЕОБРАЗОВАТЕЛЬ ГРАФИКА, поэтому вам не нужно писать K и K кодов!! (см. ссылку в конце)

3-НЕ ИСПОЛЬЗУЙТЕ ADDDBCONTEXT! Промежуточное ПО Graphql использует одноэлементный шаблон; все, что используется посредством внедрения зависимостей в graphql, является внешне синглтонным, даже если оно зарегистрировано как область действия (AddDbContext означает "область действия").

Это означает, что у вас открыто 1 соединение для запуска. Вы не можете выполнять операцию на 2 дБ одновременно!

В реальной жизни вы не можете использовать AddDbContext с Graphql!

Для этого вы можете использовать заводской шаблон. Итак, не передавайте dbcontext при внедрении зависимостей, а используйте Func и явно создавайте экземпляр dbcontext.

Вот полный пример реализации:https://github.com/graphql-dotnet/graphql-dotnet/issues/576

Я использую GraphQL.NET (graphql-dotnet) Джо Макбрайда версии 2.4.0.

В первую очередь рекомендую обновиться хотя бы до v4.6 - там много исправлений и полезных обновлений.

Во-вторых, если у вас нет мутации данных (означает - обновить/удалить/вставить данные), я бы сказал, что лучше не использовать EF для выборки данных. На основе той же библиотеки GraphQL.Net вы можете посмотреть, например, NReco.GraphQL использует облегченный ORM для извлечения и сопоставления данных (вам просто нужно определить схему в json-файле).