C# Объединение Linq работает, но тот же структурированный Linq GroupJoin завершается неудачно - PullRequest
0 голосов
/ 29 апреля 2020

У меня есть две сущности - Клиенты и Работа. Клиенты имеют от 0 до многих заданий, связанных с ними.

Клиент выглядит следующим образом:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using JobsLedger.INTERFACES;

namespace JobsLedger.DATA.ENTITIES
{
#nullable enable
    public class Client : IEntityBase, IAuditedEntityBase
    {
        public Client()
        {
            ClientNotes = new List<Note>();
            Jobs = new List<Job>();
        }

        [Key]
        public int Id { get; set; }
        public string ClientNo { get; set; } = default!;
        public bool Company { get; set; }
        public string? CompanyName { get; set; }
        public string? Abn { get; set; }
        public bool IsWarrantyCompany { set; get; }
        public bool RequiresPartsPayment { set; get; }
        public string? ClientFirstName { get; set; }
        public string ClientLastName { get; set; } = default!;
        public string? Email { get; set; }
        public string? MobilePhone { get; set; }
        public string? Phone { get; set; }
        public string? Address1 { get; set; }
        public string? Address2 { get; set; }
        public string? BankName { get; set; }
        public string? BankBSB { get; set; }
        public string? BankAccount { get; set; }
        public bool Active { get; set; }
        public DateTime? DateDeActivated { get; set; }
        public bool Activity { get; set; }

        // One warranty company client to a job.
        public int? WarrantyCompanyId { get; set; }

        public virtual Job? WarrantyCompany { get; set; }

        // One suburb to a client.
        public int? SuburbId { get; set; }
        public virtual Suburb? Suburb { get; set; }

        // If its a warranty company then we simply link it one to one to the brand id.
        public virtual Brand? Brand { get; set; }

        // Multiple notes for each client.
        public virtual ICollection<Note> ClientNotes { get; set; }

        // Multiple jobs for each client.
        public virtual ICollection<Job> Jobs { get; set; }

        public virtual ICollection<Job> WarrantyCompanyJobs { get; } = default!;
    }
#nullable disable
}

Работа выполняется следующим образом:

using System.Collections.Generic;
using JobsLedger.INTERFACES;

namespace JobsLedger.DATA.ENTITIES
{
    public class Job : IEntityBase, IAuditedEntityBase
    {
        public Job()
        {
            JobNotes = new List<Note>();
            Visits = new List<Visit>();
        }

        public string? JobNo { get; set; }
        public string? AgentJobNo { get; set; }


        public int ClientId { get; set; } = default!;
        public virtual Client Client { get; set; } = default!;

        public int? BrandId { get; set; }
        public virtual Brand? Brand { get; set; }

        public int? TypeId { get; set; }
        public virtual JobType? Type { get; set; }

        public int? StatusId { get; set; }
        public virtual Status? Status { get; set; }

        public int? WarrantyCompanyId { get; set; }
        public virtual Client? WarrantyCompany { get; set; }

        public string? Model { get; set; }
        public string? Serial { get; set; }
        public string? ProblemDetails { get; set; }
        public string? SolutionDetails { get; set; }
        public virtual ICollection<Note> JobNotes { get; set; }

        public virtual ICollection<Visit> Visits { get; }

        public int Id { get; set; }
    }
#nullable disable
}

Это Linq Join работает, и я вернуть список ClientIndexDtos.

    public IQueryable<ClientIndexDto> GetClients()
    {
        var result = this._context.Clients.Join(this._context.Jobs, c => c.Id, j => j.Id, (c, j) =>
            new ClientIndexDto
            {
                Id = c.Id,
                ClientNo = c.ClientNo,
                Active = c.Active,
                ClientFirstName = c.ClientFirstName,
                ClientLastName = c.ClientLastName,
                Company = c.Company,
                CompanyName = c.CompanyName,
                MobilePhone = c.MobilePhone,
                IsWarrantyCompany = c.IsWarrantyCompany,
                //JobsCount = j.Count().ToString(CultureInfo.CurrentCulture)
            });
        return result;
    }

Но .. Я хотел количество заданий (если есть) для каждого клиента ... поэтому я задал этот вопрос на SO и было предложено:

    public IQueryable<ClientIndexDto> GetClients()
    {
        var result = this._context.Clients.GroupJoin(this._context.Jobs, c => c.Id, j => j.Id, (c, j) =>
            new ClientIndexDto
            {
                Id = c.Id,
                ClientNo = c.ClientNo,
                Active = c.Active,
                ClientFirstName = c.ClientFirstName,
                ClientLastName = c.ClientLastName,
                Company = c.Company,
                CompanyName = c.CompanyName,
                MobilePhone = c.MobilePhone,
                IsWarrantyCompany = c.IsWarrantyCompany,
                JobsCount = j.Count().ToString(CultureInfo.CurrentCulture)
            });
        return result;
    }

Хотя это работает для версии соединения, я получаю следующую ошибку при запуске с groupJoin ..

The LINQ expression 'DbSet<Client>()
    .GroupJoin(
        inner: DbSet<Job>(), 
        outerKeySelector: c => c.Id, 
        innerKeySelector: j => j.Id, 
        resultSelector: (c, j) => new ClientIndexDto{ 
            Id = c.Id, 
            ClientNo = c.ClientNo, 
            Active = c.Active, 
            ClientFirstName = c.ClientFirstName, 
            ClientLastName = c.ClientLastName, 
            Company = c.Company, 
            CompanyName = c.CompanyName, 
            MobilePhone = c.MobilePhone, 
            IsWarrantyCompany = c.IsWarrantyCompany 
        }
    )' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Я отмечаю, что URL - https://go.microsoft.com/fwlink/?linkid=2101038 обсуждает оценку клиент-сервер ... ну, естественно, я хочу, чтобы это происходило в базе данных, но я озадачен тем, почему один (Join) работает плавно, а другой (GroupJoin) не работает.

Может кто-нибудь ответить, почему сначала он не работает, а затем сказать мне, что мне нужно сделать, чтобы это исправить. Я бы использовал Join, но мне нужно знать, сколько рабочих мест существует на каждом клиенте. GroupJoin сообщит мне, что если я смогу заставить его работать ...

Я знаю, что хотел бы, чтобы он выполнялся в конце базы данных ie, получил IQueryable et c, чтобы он не перетаскивался больше записей обратно клиенту, чем необходимо.

Любая помощь приветствуется.

1 Ответ

1 голос
/ 29 апреля 2020

Вы должны знать о разнице между объектом, который реализует IQueryable, и объектом, который реализует IEnumerable.

. IEnumerable представляет последовательность похожих элементов. Вы можете получить первый элемент последовательности, и как только вы получите такой элемент, вы можете получить следующий элемент.

Обычно это делается с помощью методов foreach или LINQ, таких как ToList (), Count ( ) Any (), FirstOrDefault () и т. Д. c. В глубине души все они используют GetEnumerator () и MoveNext () / Current.

С другой стороны, объект, который реализует IQueryable, представляет потенциал для создания объекта IEnumerable . Он не представляет сам объект IEnumerable.

* IQueryable содержит Expression и Provider. Expression - это обобщенная форма c того, что должно быть запрошено. Provider знает, кто должен выполнить запрос (обычно это система управления базами данных) и на каком языке использует эта СУБД (обычно SQL).

Как только вы начнете перечислять IQueryable (GetEnumerator), Expression отправляется Provider, который переведет Expression в SQL и попросит СУБД выполнить запрос. Возвращенные данные представлены как IEnumerator, поэтому вы можете вызвать MoveNext / Current.

Какое отношение это имеет к моей проблеме?

Проблема в том, что Поставщик должен знать, как перевести ваше выражение в SQL. Хотя он довольно умен в переводе выражений, его функциональность ограничена. Он не знает ваших собственных классов и методов; Поставщик не может перевести их на SQL. На самом деле есть несколько методов LINQ, которые не поддерживаются. См. Поддерживаемые и неподдерживаемые методы LINQ (LINQ to Entities) .

В вашем случае проблема заключается в том, что ваш провайдер не знает, как перевести ToString(CultureInfo.CurrentCulture) в SQL. Он не знает класс CultureInfo.

Так что я не могу вернуть свой JobsCount в виде строки?

Нет, вы не можете.

Обычно это не проблема, потому что не стоит помещать число в строку. Кроме того, все скажут, что JobsCount - это число, а не какой-то текст, это создаст проблемы пользователям вашего метода, если они захотят что-то сделать с возвращенным JobsCount, например, подсчитают среднее значение JobsCount в течение месяца.

Единственная причина для преобразования чисел в строки состоит в том, что люди не могут очень хорошо интерпретировать биты, они намного лучше интерпретируют текстовое представление компьютерных чисел.

Следовательно: правильная схема будет заключаться в том, что числа хранятся в целых, десятичных, двойных и т. д. c.

Непосредственно перед отображением они преобразуются в текстовое представление. То же самое, если для ввода пользователя: пользователь вводит некоторый текст. Это проверено на правильность и переведено в число. После этого остается номер. Преимущество заключается в том, что вы уверены, что после преобразования его в число вам больше никогда не придется проверять правильность формата.

Кроме того: числа для компьютеров гораздо эффективнее, чем строки.

Итак, мой совет: сохраните его в виде числа и преобразуйте его в текст только тогда, когда вам необходимо: отобразить его, сохранить в текстовом файле (json, xml), чтобы общаться через inte rnet et c. Попробуйте выполнить преобразование как можно позже.

Но я не могу изменить свой класс, я должен преобразовать его в строку!

Ну, в этом case: получить ваши данные как число и использовать AsEnumerable(). Это переместит ваш номер в локальный процесс. Там вы можете конвертировать свой номер, используя любой локальный код, который вы хотите.

var result = this._context.Clients.GroupJoin((client, jobsForThisClient) => new
{
    Id = client.Id,
    ClientNo = client.ClientNo,
    ...

    JobsCount = jobsForThisClient.Count(),
})

// Execute the query and move the fetched data to local process
AsEnumerable()

// put the fetched data in a ClientIndexDto
.Select(fetchedData => new ClientIndexDto
{
    Id = fetchedData .Id,
    ClientNo = fetchedData .ClientNo,
    ...

    JobsCount = fetchedData.JobsCount.ToString(CultureInfo.CurrentCulture),
});

Это не менее эффективно. На самом деле: это может быть даже более эффективно, поскольку ваш JobsCount передается как Int32: только четыре байта. Большинство текстовых представлений JobsCount занимает более четырех байтов.

Не забудьте поразить дизайнера, который думал, что JobsCount - это кусок текста.

...