为什么 Entity Framework 在使用 Linq .Union() 或 .Except() 时生成错误的列名?

Why Entity Framework generates bad column names when using Linq .Union() or .Except()?

我在我的项目中使用 EF 6。我只想从数据库中获取一些数据,有时加入额外的数据。

这是我的代码:

var queryable = dbContext.Manuals
    .AsNoTracking()
    .Where(x => x.Status == "ACTIVE");

if (showDisabledParents)
{
    var parentsIds = queryable
        .Where(x => x.ParentId.HasValue)
        .Select(x => x.ParentId.Value)
        .Distinct()
        .ToArray();

    queryable = queryable.Union(
        dbContext.Manuals
            .AsNoTracking()
            .Where(x => parentsIds.Contains(x.Id))
    );
}

showDisabledParentsfalse 时效果很好。 EF 将生成具有正确列名的 SQL 查询。

但是当 showDisabledParentstrue 时,EF 生成 UNION 语句(这是预期的)但它使用 C1、C2、C3 .. . 对于列名。

问题是我有一个自定义 DbDataReader,它调用 DateTime.SpecifyKind(..., DateTimeKind.Utc) 如果列名以 "Utc" 结尾。由于 EF 使用了错误的列名(C1、C2、C3 等),我的逻辑无法正常工作。

是否可以通过某种方式阻止这种行为?我的意思是,如果有办法告诉 EF 不要使用这些奇怪的列名。

更新: 这是我的 DBDataReaderCode:

public class MyDbDataReader : DelegatingDbDataReader
{

private string _dbName;
public MyDbDataReader(string dbName, DbDataReader source)
    : base(source)
{
    _dbName = dbName;
}

public override DateTime GetDateTime(int ordinal)
{   
    return DateTime.SpecifyKind(base.GetDateTime(ordinal), base.GetName(ordinal).EndsWith("UTC", StringComparison.OrdinalIgnoreCase) 
        ? DateTimeKind.Utc 
        : DateTimeKind.Local);       
}

}

我认为这是不可能的,因为 EF 使用内部别名来生成整个查询,这样可以确保从不同表等收集的名称中没有重复项。

无论如何,我宁愿搜索有关您处理日期的问题。您能否提供有关您的案例的更多详细信息?一般来说,经验法则是始终以 UTC 格式保留日期并根据需要转换为特定时区。

我用于处理 DateTime.Kind 的典型方法是在适用的实体 属性 上使用属性。例如,这将在任何 UTC:

的 DateTime 实体 属性 上进行
    [DateTimeKind(DateTimeKind.Utc)]
    public DateTime SomeDateUTC { get; set; }

如果您希望将其他日期时间标记为当地时间:

    [DateTimeKind(DateTimeKind.Local)]
    public DateTime SomeDate { get; set; }

没有属性的 DateTimes 将保留为未指定类型。

属性本身:参见 (Entity Framework DateTime and UTC)

然后在您的 DbContext 中,您只需将其添加到 InitializeContext 方法中:

((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
    (sender, e) => DateTimeKindAttribute.Apply(e.Entity);

否则,如果你想坚持你现在的方法,而且你依赖于列名,Union 不能指望,所以将条件拆分为两个查询。这将需要在执行查询的任何地方完成,因此如果您的方法返回 IQueryable<Manual>,则需要将其更改为 IEnumerable<IQueryable<Manual>>,如果返回多个查询,则合并结果。

var queryables = new List<IQueryable<Manual>();

queryables.Add(dbContext.Manuals
    .AsNoTracking()
    .Where(x => x.Status == "ACTIVE"));

if (showDisabledParents)
{
    var parentsIds = dbContext.Manuals
        .Where(x => x.Status == "ACTIVE"
            && x.ParentId.HasValue)
        .Select(x => x.ParentId.Value)
        .Distinct()
        .ToArray();

    queryables.Add = dbContext.Manuals
            .AsNoTracking()
            .Where(x => parentsIds.Contains(x.Id));
    
}

return queryables;

如果您改为在此方法中执行查询,则只需捕获第二个查询(如果需要)并合并要返回的结果。

这里最棘手的一点是,如果您需要对组合集进行分页。在这种情况下,我会改为使用 two-pass 方法,在适当排序后获取相关任务 ID,然后对 ID 使用分页提取,按 ID 加载实体:

var queryable = dbContext.Manuals
    .AsNoTracking()
    .Where(x => x.Status == "ACTIVE");

if (showDisabledParents)
{
    var parentsIds = queryable
        .Where(x => x.ParentId.HasValue)
        .Select(x => x.ParentId.Value)
        .Distinct()
        .ToArray();

    queryable = queryable.Union(
        dbContext.Manuals
            .AsNoTracking()
            .Where(x => parentsIds.Contains(x.Id))
    );
}

var ids = queryable
    .OrderBy(/* condition */)
    .Select(x => x.Id)
    .Skip(page * pageSize)
    .Take(pageSize)
    .ToList(); // Should execute union without worrying about column transformation since we have requested only IDs.

var manuals = await dbContext.Manuals.Where(x => ids.Contains(x.Id)).ToListAsync();

至少要考虑几个选项。