C# Linq join 有效,但相同结构的 Linq GroupJoin 失败
C# Linq join works but the same structured Linq GroupJoin fails
我有两个实体 - 客户和工作。客户有 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 讨论了客户端 - 服务器评估...好吧,我自然希望这发生在数据库上,但我很困惑为什么一个(加入)可以流畅地工作而另一个 (GroupJoin) 步履蹒跚。
有人可以先回答为什么它不起作用,然后告诉我我需要做什么来修复它。我会使用 Join 除非我需要知道每个客户端上存在多少个作业。 GroupJoin 会告诉我,如果我能让它正常工作...
我知道我希望它在数据库端执行,即获取一个 IQueryable 等,这样它就不会拖回客户端不必要的记录..
感谢任何帮助。
您必须了解实现 IQueryable
的对象与实现 IEnumerable
的对象之间的区别。
一个 IEnumerable
表示一系列相似的项目。可以得到序列的第一个元素,得到这样一个元素后,就可以得到下一个元素。
这通常是使用 foreach 或 LINQ 方法(如 ToList()、Count()、Any()、FirstOrDefault() 等)完成的。在深处,它们都使用 GetEnumerator() 和 MoveNext() / Current。
另一方面,实现 IQueryable
的对象代表 创建 IEnumerable 对象的潜力 。它不代表 IEnumerable 对象本身。
IQueryable
持有一个 Expression
和一个 Provider
。 Expression
是必须查询的通用形式。 Provider
知道谁必须执行查询(通常是数据库管理系统)以及该 DBMS 使用什么语言(通常 SQL)。
一旦您开始枚举 IQueryable
(GetEnumerator),Expression
就会发送给 Provider
,后者会将 Expression
翻译成 SQL 并要求 DBMS 执行查询。 returned 数据表示为 IEnumerator
,因此您可以调用 MoveNext / Current.
这与我的问题有什么关系?
问题是提供者需要知道如何将您的表达式翻译成 SQL。虽然它在如何翻译表达式方面相当聪明,但它的功能有限。它不知道你自己的 classes 和方法;提供者无法将它们翻译成 SQL。事实上,有几种 LINQ 方法不受支持。参见 Supported and Unsupported LINQ Methods (LINQ to Entities)。
在你的情况下,问题是你的提供者不知道如何将 ToString(CultureInfo.CurrentCulture)
翻译成 SQL。它不知道 class CultureInfo
.
所以我不能 return 我的 JobsCount 作为字符串?
不,你不能。
通常这不是问题,因为将数字放在字符串中是不明智的。除此之外,每个人都会说 JobsCount 是一个数字,而不是一些文本,如果他们想用 returned JobsCount 做一些事情,这会给你的方法的用户带来麻烦,例如计算一个期间的平均 JobsCount月。
将数字转换为字符串的唯一原因是因为人类不能很好地解释位,他们更好地解释计算机数字的文本表示。
因此:正确的设计是将数字存储在整数、小数、双精度等中。
就在显示之前,它们被转换为文本表示。对于用户输入也是如此:用户键入一些文本。检查其正确性并转换为数字。之后它仍然是一个数字。优点是您可以确定,一旦将其转换为数字,您将永远不必再次检查格式的正确性。
此外:数字对于计算机来说比字符串更有效。
所以我的建议是:将其保留为数字,仅在需要时才将其转换为文本:显示它、将其保存在文本文件中(json、xml ), 通过 Internet 等进行通信。尝试尽可能晚地进行转换。
但是我不能改变我的class,我必须把它转换成字符串!
那么,在那种情况下:以数字形式获取数据,然后使用 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 是一段文字的设计师。
我有两个实体 - 客户和工作。客户有 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 讨论了客户端 - 服务器评估...好吧,我自然希望这发生在数据库上,但我很困惑为什么一个(加入)可以流畅地工作而另一个 (GroupJoin) 步履蹒跚。
有人可以先回答为什么它不起作用,然后告诉我我需要做什么来修复它。我会使用 Join 除非我需要知道每个客户端上存在多少个作业。 GroupJoin 会告诉我,如果我能让它正常工作...
我知道我希望它在数据库端执行,即获取一个 IQueryable 等,这样它就不会拖回客户端不必要的记录..
感谢任何帮助。
您必须了解实现 IQueryable
的对象与实现 IEnumerable
的对象之间的区别。
一个 IEnumerable
表示一系列相似的项目。可以得到序列的第一个元素,得到这样一个元素后,就可以得到下一个元素。
这通常是使用 foreach 或 LINQ 方法(如 ToList()、Count()、Any()、FirstOrDefault() 等)完成的。在深处,它们都使用 GetEnumerator() 和 MoveNext() / Current。
另一方面,实现 IQueryable
的对象代表 创建 IEnumerable 对象的潜力 。它不代表 IEnumerable 对象本身。
IQueryable
持有一个 Expression
和一个 Provider
。 Expression
是必须查询的通用形式。 Provider
知道谁必须执行查询(通常是数据库管理系统)以及该 DBMS 使用什么语言(通常 SQL)。
一旦您开始枚举 IQueryable
(GetEnumerator),Expression
就会发送给 Provider
,后者会将 Expression
翻译成 SQL 并要求 DBMS 执行查询。 returned 数据表示为 IEnumerator
,因此您可以调用 MoveNext / Current.
这与我的问题有什么关系?
问题是提供者需要知道如何将您的表达式翻译成 SQL。虽然它在如何翻译表达式方面相当聪明,但它的功能有限。它不知道你自己的 classes 和方法;提供者无法将它们翻译成 SQL。事实上,有几种 LINQ 方法不受支持。参见 Supported and Unsupported LINQ Methods (LINQ to Entities)。
在你的情况下,问题是你的提供者不知道如何将 ToString(CultureInfo.CurrentCulture)
翻译成 SQL。它不知道 class CultureInfo
.
所以我不能 return 我的 JobsCount 作为字符串?
不,你不能。
通常这不是问题,因为将数字放在字符串中是不明智的。除此之外,每个人都会说 JobsCount 是一个数字,而不是一些文本,如果他们想用 returned JobsCount 做一些事情,这会给你的方法的用户带来麻烦,例如计算一个期间的平均 JobsCount月。
将数字转换为字符串的唯一原因是因为人类不能很好地解释位,他们更好地解释计算机数字的文本表示。
因此:正确的设计是将数字存储在整数、小数、双精度等中。
就在显示之前,它们被转换为文本表示。对于用户输入也是如此:用户键入一些文本。检查其正确性并转换为数字。之后它仍然是一个数字。优点是您可以确定,一旦将其转换为数字,您将永远不必再次检查格式的正确性。
此外:数字对于计算机来说比字符串更有效。
所以我的建议是:将其保留为数字,仅在需要时才将其转换为文本:显示它、将其保存在文本文件中(json、xml ), 通过 Internet 等进行通信。尝试尽可能晚地进行转换。
但是我不能改变我的class,我必须把它转换成字符串!
那么,在那种情况下:以数字形式获取数据,然后使用 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 是一段文字的设计师。