【问题标题】:C# Linq join works but the same structured Linq GroupJoin failsC# Linq 加入工作,但相同的结构化 Linq GroupJoin 失败
【发布时间】:2020-04-29 06:53:22
【问题描述】:

我有两个实体 - 客户和工作。客户有 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;
    }

但是..我想要每个客户的工作数量(如果有的话)...所以我问了this question on 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 会给我,如果我可以让它工作......

我知道我希望它在数据库端执行,即获取一个 IQueryable 等,这样它就不会将更多的记录拖回客户端。

任何帮助表示赞赏。

【问题讨论】:

    标签: c# linq linq-to-sql entity-framework-core


    【解决方案1】:

    您必须了解实现IQueryable 的对象和实现IEnumerable 的对象之间的区别。

    IEnumerable 表示一系列相似项目。你可以得到序列的第一个元素,一旦你得到了这样一个元素,你就可以得到下一个元素。

    这通常使用 foreach 或 LINQ 方法完成,如 ToList()、Count()、Any()、FirstOrDefault() 等。在内部,它们都使用 GetEnumerator() 和 MoveNext() / Current。

    另一方面,实现IQueryable 的对象代表有可能创建一个 IEnumerable 对象。它并不代表 IEnumerable 对象本身。

    IQueryable 包含一个 Expression 和一个 ProviderExpression 是必须查询的通用形式。 Provider 知道谁必须执行查询(通常是数据库管理系统)以及该 DBMS 使用什么语言(通常是 SQL)。

    一旦您开始枚举IQueryable (GetEnumerator),Expression 就会发送到ProviderProvider 会将Expression 转换为 SQL 并要求 DBMS 执行查询。返回的数据表示为IEnumerator,因此您可以调用 MoveNext / Current。

    这与我的问题有什么关系?

    问题在于提供者需要知道如何将你的表达式翻译成 SQL。尽管它在如何翻译表达式方面非常聪明,但它的功能有限。它不知道你自己的类和方法; Provider 无法将它们转换为 SQL。事实上,有几种 LINQ 方法不受支持。见Supported and Unsupported LINQ Methods (LINQ to Entities)

    在您的情况下,问题在于您的 Provider 不知道如何将 ToString(CultureInfo.CurrentCulture) 转换为 SQL。它不知道类CultureInfo

    所以我不能将 JobsCount 作为字符串返回?

    不,你不能。

    通常这不是问题,因为将数字放在字符串中是不明智的。除此之外,每个人都会说 JobsCount 是一个数字,而不是一些文本,如果他们想对返回的 JobsCount 做一些事情,例如计算一个月内的平均 JobsCount,它会给你方法的用户带来麻烦。

    将数字转换为字符串的唯一原因是因为人类不能很好地解释位,他们更好地解释计算机数字的文本表示。

    因此:正确的设计是将数字存储在整数、小数、双精度等中。

    就在显示之前,它们被转换为文本表示。如果用户输入也一样:用户键入一些文本。检查正确性并将其转换为数字。之后它仍然是一个数字。这样做的好处是您可以确定,一旦将其转换为数字,您就无需再次检查格式的正确性。

    此外:对于计算机来说,数字比字符串更有效。

    所以我的建议是:保留一个数字,仅在需要时将其转换为文本:显示、保存在文本文件(json、xml)中、通过互联网进行通信等。尝试尽可能晚地进行转换。

    但我不能更改我的类,我必须将其转换为字符串!

    好吧,在这种情况下:以数字形式获取您的数据,并使用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 是一段文字的设计师。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-25
      • 1970-01-01
      • 2020-03-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多