You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core 6迁移中可翻译属性的查询优化方案咨询

EF Core 6迁移中多语言翻译字段的查询优化问题

问题描述

我正在将一个遗留项目的ORM映射器迁移至EF Core 6,遇到了多语言翻译字段的处理难题。项目中有一张通用Text表存储所有实体的多语言翻译数据,该表无外键,文化语言由当前登录Person的配置动态决定。

我需要仅获取公司的本地化Name,而非所有Text条目,且不能使用懒加载以避免影响应用性能。目前通过向Automapper的ProjectTo方法传递参数实现了需求,但生成的SQL存在重复子查询,现寻求可复用公司/地址解析逻辑的优化方案。

相关代码示例

查询代码

var users = await _context.Person
    .AsNoTracking()
    .IsActive(_dateTimeProvider.UtcNow)
    .BySubjectId(request.SubjectId)
    .ProjectTo<UserDTO>(_mapper.ConfigurationProvider, new { timeStamp = _dateTimeProvider.UtcNow, context = _context })
    .SingleOrDefaultAsync(cancellationToken);

映射代码

private class Mapping : Profile
{
        public Mapping()
        {
            DateTime? timeStamp = null;
            IRoomsDbContext context = null;

            CreateMap<Person, User>()
                .ForMember(d => d.City, o => o.MapFrom(p => p.CompanyPerson.SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= timeStamp) && (!fp.Ende.HasValue || fp.Ende.Value > timeStamp)).Company.Adresse.Single(a => a.IstStandard).Ort))
                .ForMember(d => d.CompanyDescription, o => o.MapFrom(p => context.Text.SingleOrDefault(t => t.LanguageId == "de" && t.FieldOid == p.CompanyPerson.SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= timeStamp) && (!fp.Ende.HasValue || fp.Ende.Value > timeStamp)).Company.BezeichnungOid).Value))
                .ForMember(d => d.Zip, o => o.MapFrom(p => p.CompanyPerson.SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= timeStamp) && (!fp.Ende.HasValue || fp.Ende.Value > timeStamp)).Firma.Adresse.Single(a => a.IstStandard).Plz));
        }
}

生成的SQL代码

DECLARE @__cutoffDate_0 datetime = '2023-11-03 10:23:14.960'
DECLARE @__timeStamp_1 datetime = '2023-11-03 10:23:14.960'

SELECT
    [p].[Id],
    (SELECT TOP(1) [a].[Ort]
    FROM [Adresse] AS [a]
    WHERE (((
        SELECT TOP(1) [f1].[Id]
        FROM [CompanyPerson] AS [f0]
        INNER JOIN [Company] AS [f1] ON [f0].[CompanyId] = [f1].[Id]
        WHERE ([p].[Id] = [f0].[PersonId]) AND ((([f0].[Beginn] IS NULL) OR ([f0].[Beginn] <= @__timeStamp_1)) AND (([f0].[Ende] IS NULL) OR ([f0].[Ende] > @__timeStamp_1)))) IS NOT NULL) AND ((
        SELECT TOP(1) [f3].[Id]
        FROM [CompanyPerson] AS [f2]
        INNER JOIN [Company] AS [f3] ON [f2].[CompanyId] = [f3].[Id]
        WHERE ([p].[Id] = [f2].[PersonId]) AND ((([f2].[Beginn] IS NULL) OR ([f2].[Beginn] <= @__timeStamp_1)) AND (([f2].[Ende] IS NULL) OR ([f2].[Ende] > @__timeStamp_1)))) = [a].[CompanyId])) AND ([a].[IstStandard] = CAST(1 AS bit))
    ) AS [City],
    (SELECT TOP(1) [a0].[Plz]
        FROM [Adresse] AS [a0]
        WHERE (((
            SELECT TOP(1) [f7].[Id]
            FROM [CompanyPerson] AS [f6]
            INNER JOIN [Company] AS [f7] ON [f6].[CompanyId] = [f7].[Id]
            WHERE ([p].[Id] = [f6].[PersonId]) AND ((([f6].[Beginn] IS NULL) OR ([f6].[Beginn] <= @__timeStamp_1)) AND (([f6].[Ende] IS NULL) OR ([f6].[Ende] > @__timeStamp_1)))) IS NOT NULL) AND ((
            SELECT TOP(1) [f9].[Id]
            FROM [CompanyPerson] AS [f8]
            INNER JOIN [Company] AS [f9] ON [f8].[CompanyId] = [f9].[Id]
            WHERE ([p].[Id] = [f8].[PersonId]) AND ((([f8].[Beginn] IS NULL) OR ([f8].[Beginn] <= @__timeStamp_1)) AND (([f8].[Ende] IS NULL) OR ([f8].[Ende] > @__timeStamp_1)))) = [a0].[CompanyId])) AND ([a0].[IstStandard] = CAST(1 AS bit))
    ) AS [Zip],
    (SELECT TOP(1) [t].[Value]
    FROM [Text] AS [t]
    WHERE ([t].[LanguageId] = 'de') AND ([t].[FieldOid] = (
        SELECT TOP(1) [f5].[BezeichnungOid]
        FROM [CompanyPerson] AS [f4]
        INNER JOIN [Company] AS [f5] ON [f4].[CompanyId] = [f5].[Id]
        WHERE ([p].[Id] = [f4].[PersonId]) AND ((([f4].[Beginn] IS NULL) OR ([f4].[Beginn] <= @__timeStamp_1)) AND (([f4].[Ende] IS NULL) OR ([f4].[Ende] > @__timeStamp_1)))))
    ) AS [CompanyName]
FROM [Person] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM [CompanyPerson] AS [f]
    WHERE ([p].[Id] = [f].[PersonId]) AND ((([f].[Beginn] IS NULL) OR ([f].[Beginn] <= @__cutoffDate_0)) AND (([f].[Ende] IS NULL) OR ([f].[Ende] >= @__cutoffDate_0))))

优化方案

1. 直接使用EF Core投影替代Automapper复杂映射

在查询阶段通过Select直接构造DTO,EF Core会自动优化SQL生成逻辑,避免重复子查询:

var users = await _context.Person
    .AsNoTracking()
    .IsActive(_dateTimeProvider.UtcNow)
    .BySubjectId(request.SubjectId)
    .Select(p => 
    {
        var activeCompanyPerson = p.CompanyPerson
            .SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= _dateTimeProvider.UtcNow) 
                                && (!fp.Ende.HasValue || fp.Ende.Value > _dateTimeProvider.UtcNow));
        var standardAddress = activeCompanyPerson?.Company.Adresse.SingleOrDefault(a => a.IstStandard);
        var companyName = _context.Text
            .SingleOrDefault(t => t.LanguageId == "de" 
                                && t.FieldOid == activeCompanyPerson?.Company.BezeichnungOid)?.Value;
        return new UserDTO
        {
            Id = p.Id,
            City = standardAddress?.Ort,
            Zip = standardAddress?.Plz,
            CompanyDescription = companyName
        };
    })
    .SingleOrDefaultAsync(cancellationToken);

2. 封装复用逻辑到扩展方法

把获取有效公司、标准地址、本地化名称的逻辑封装成通用扩展方法,方便在多个查询场景复用:

public static class EntityExtensions
{
    public static CompanyPerson GetActiveCompanyPerson(this Person person, DateTime timeStamp)
    {
        return person.CompanyPerson.SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= timeStamp) 
                                                      && (!fp.Ende.HasValue || fp.Ende.Value > timeStamp));
    }

    public static Adresse GetStandardAddress(this Company company)
    {
        return company.Adresse.SingleOrDefault(a => a.IstStandard);
    }

    public static string GetLocalizedName(this IRoomsDbContext context, string fieldOid, string languageId)
    {
        return context.Text.SingleOrDefault(t => t.LanguageId == languageId && t.FieldOid == fieldOid)?.Value;
    }
}

调用示例:

var users = await _context.Person
    .AsNoTracking()
    .IsActive(_dateTimeProvider.UtcNow)
    .BySubjectId(request.SubjectId)
    .Select(p => 
    {
        var activeCompany = p.GetActiveCompanyPerson(_dateTimeProvider.UtcNow);
        var standardAddress = activeCompany?.Company.GetStandardAddress();
        var companyName = _context.GetLocalizedName(activeCompany?.Company.BezeichnungOid, "de");
        return new UserDTO
        {
            Id = p.Id,
            City = standardAddress?.Ort,
            Zip = standardAddress?.Plz,
            CompanyDescription = companyName
        };
    })
    .SingleOrDefaultAsync(cancellationToken);

3. 使用Automapper自定义值解析器

创建自定义值解析器封装重复逻辑,替代映射中的硬编码子查询,让Automapper生成更高效的SQL:

public class CompanyCityResolver : IValueResolver<Person, UserDTO, string>
{
    private readonly DateTime _timeStamp;

    public CompanyCityResolver(DateTime timeStamp)
    {
        _timeStamp = timeStamp;
    }

    public string Resolve(Person source, UserDTO destination, string destMember, ResolutionContext context)
    {
        var activeCompanyPerson = source.CompanyPerson.SingleOrDefault(fp => (!fp.Beginn.HasValue || fp.Beginn.Value <= _timeStamp) 
                                                                       && (!fp.Ende.HasValue || fp.Ende.Value > _timeStamp));
        return activeCompanyPerson?.Company.Adresse.SingleOrDefault(a => a.IstStandard)?.Ort;
    }
}

// 同理创建CompanyZipResolver和CompanyNameResolver

修改映射配置:

private class Mapping : Profile
{
    public Mapping()
    {
        CreateMap<Person, UserDTO>()
            .ForMember(d => d.City, o => o.MapFrom<CompanyCityResolver, DateTime>(_ => _timeStamp))
            .ForMember(d => d.Zip, o => o.MapFrom<CompanyZipResolver, DateTime>(_ => _timeStamp))
            .ForMember(d => d.CompanyDescription, o => o.MapFrom<CompanyNameResolver, object>(_ => new { TimeStamp = _timeStamp, Context = _context }));
    }
}

内容的提问来源于stack exchange,提问作者Lukas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 06:52:04