多组连接 lambda 到表达式树
Posted
技术标签:
【中文标题】多组连接 lambda 到表达式树【英文标题】:Multiple group join lambda to expression tree 【发布时间】:2014-09-10 17:53:48 【问题描述】:在我们的数据库中,我们有许多表,它们具有相应的翻译表,语言和地区 ID(映射到其他表),语言 1 是英语,语言 1 的默认地区是英国。所有具有转换表的表都具有以下默认列(尽管实体框架类上没有定义接口):
<EntityTableName>
EntityTableNameID INT PK
Reference NVARCHAR NULL
[Any other columns]
<EntityTableNameTranslation>
EntityTableNameID INT NOT NULL
LanguageID INT NOT NULL
RegionID INT NULL
Title NVARCHAR NOT NULL
Description NVARCHAR NULL
整个数据库的命名是一致的,所以如果需要我们可以添加接口,但现在我一直在尝试这样做而不是为了节省工作量。
确定返回哪个翻译标题和描述的逻辑是: 1)如果语言和地区都完全匹配,则返回 2) 如果语言匹配,但地区不匹配,则返回该语言的“默认”(RegionID 为空,并且每种语言总是有一个) 3) 如果语言不匹配,则返回系统默认值(LanguageID = 1,RegionID IS NULL)。
我知道这听起来可能很奇怪,每个人都有更好的方法来做这件事,但这是我必须处理的简短内容。所以这是我创建的 lambda 组连接函数,它使用数据库中名为“OrgGroup”的实体:
public static IEnumerable<TransViewModel> GetUserAreaOrgGroups(TransTestEntities context, int companyID, int languageID, int? regionID)
var transFull = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
var transLang = context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && !tr.RegionID.HasValue);
var transDefault = context.OrgGroupTranslations.Where(tr => tr.LanguageID == 1 && !tr.RegionID.HasValue);
var results = context.OrgGroups.Where(en => en.CompanyID == companyID)
.GroupJoin(transFull, en => en.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> Entity = en, TransFull = tr.DefaultIfEmpty().FirstOrDefault(), TransLang = null, TransDefault = null)
.GroupJoin(transLang, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> Entity = en.Entity, TransFull = en.TransFull, TransLang = tr.DefaultIfEmpty().FirstOrDefault(), TransDefault = null )
.GroupJoin(transDefault, en => en.Entity.OrgGroupID, tr => tr.OrgGroupID,
(en, tr) => new TransJoin<OrgGroup, OrgGroupTranslation> Entity = en.Entity, TransFull = en.TransFull, TransLang = en.TransLang, TransDefault = tr.DefaultIfEmpty().FirstOrDefault() )
.Select(vm => new TransViewModel
EntityID = vm.Entity.OrgGroupID,
Title = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Title,
Description = (vm.TransFull ?? vm.TransLang ?? vm.TransDefault).Description
);
return results;
这似乎按预期工作,现在我试图将其转换为一个函数,该函数将接受两种表类型并使用表达式树来创建、执行和返回等效查询。据我所知:
public static IEnumerable<TransViewModel> GetUserAreaTranslations<TEntity, TTrans>(TransTestEntities context, int companyID, int languageID, int? regionID)
// Get types
Type entityType = typeof(TEntity);
Type transType = typeof(TTrans);
string entityName = entityType.Name;
string transName = transType.Name;
// Parameters
var entityParam = Expression.Parameter(entityType, "en");
var transParam = Expression.Parameter(transType, "tr");
var combinedParam = new ParameterExpression[] entityParam, transParam ;
// Properties
var CompanyIDProp = Expression.Property(entityParam, "CompanyID");
var entityIDProp = Expression.Property(entityParam, entityName + "ID");
var transIDProp = Expression.Property(transParam, entityName + "ID");
var transLanProp = Expression.Property(transParam, "LanguageID");
var transRegProp = Expression.Property(transParam, "RegionID");
var transTitleProp = Expression.Property(transParam, "Title");
var transDescProp = Expression.Property(transParam, "Description");
// Tables
//TODO: Better way of finding pluralised table names
var entityTable = Expression.PropertyOrField(Expression.Constant(context), entityName + "s");
var transTable = Expression.PropertyOrField(Expression.Constant(context), transName + "s");
// Build translation subqueries
//e.g. context.OrgGroupTranslations.Where(tr => tr.LanguageID == languageID && tr.RegionID == regionID);
MethodCallExpression fullTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] transType ,
new Expression[]
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.Equal(transRegProp, Expression.Convert(Expression.Constant(languageID), transRegProp.Type))
), transParam
)
)
);
MethodCallExpression lanTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] transType ,
new Expression[]
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(languageID)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
);
MethodCallExpression defaultTranWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] transType ,
new Expression[]
transTable,
Expression.Quote
(
Expression.Lambda
(
Expression.AndAlso
(
Expression.Equal(transLanProp, Expression.Constant(1)),
Expression.IsFalse(MemberExpression.Property(transRegProp, "HasValue"))
), transParam
)
)
);
MethodCallExpression entityWhereLambda = Expression.Call(typeof(Queryable),
"Where",
new Type[] entityType ,
new Expression[]
entityTable,
Expression.Quote(
Expression.Lambda
(
Expression.Equal(CompanyIDProp, Expression.Convert(Expression.Constant(companyID), CompanyIDProp.Type))
, entityParam
)
)
);
// Create the "left join" call:
// tr.DefaultIfEmpty().FirstOrDefault()
var joinType = typeof(TransJoin<TEntity, TTrans>);
var joinParam = Expression.Parameter(joinType, "tr");
var leftJoinMethods =
Expression.Call(
typeof(Enumerable),
"FirstOrDefault",
new Type[] transType ,
Expression.Call(
typeof(Enumerable),
"DefaultIfEmpty",
new Type[] transType ,
Expression.Parameter(typeof(IEnumerable<TTrans>), "tr"))
);
// Create the return bindings
var emptyTrans = Expression.Constant(null, typeof(TTrans));
//var emptyTrans = Expression.Constant(null);
var fullBindings = new List<MemberBinding>();
fullBindings.Add(Expression.Bind(joinType.GetProperty("Entity"), entityParam));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransFull"), leftJoinMethods));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransLang"), emptyTrans));
fullBindings.Add(Expression.Bind(joinType.GetProperty("TransDefault"), emptyTrans));
// Create an object initialiser which also sets the properties
Expression fullInitialiser = Expression.MemberInit(Expression.New(joinType), fullBindings);
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda <Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
// Create first group join
var fullJoin = Expression.Call(
typeof(Queryable),
"GroupJoin",
new Type[]
typeof (TEntity), // TOuter,
typeof (TTrans), // TInner,
typeof (int), // TKey,
typeof (TransJoin<TEntity, TTrans>) // TResult
,
new Expression[]
entityWhereLambda,
fullTranWhereLambda,
Expression.Lambda<Func<TEntity, int>>(entityIDProp, entityParam),
Expression.Lambda<Func<TTrans, int>>(transIDProp, transParam),
fullResultSelector
);
问题是 groupjoin 期望返回一个 TTrans 的 IEnumerable,我似乎无法绑定它,并且我无法将其更改为标准连接,因为我将无法使用在投影中合并,因为不会返回任何结果。
我确定我在做一些非常愚蠢的事情,所以有人可以帮我让我的小组加入工作吗?
【问题讨论】:
【参考方案1】:您要查找的表达式节点是 MemberInitExpression
,它是编译包含 new
语句的 lambda 的结果。
假设我们有一个像这样的简单键值类:
public class KV
public int Key;
public string Value;
我可以为此构建一个new
表达式来加载一些常量,如下所示:
Type tKV = typeof(KV);
MemberInfo miKey = tKV.GetMember("Key")[0];
MemberInfo miValue = tKV.GetMember("Value")[0];
Expression meminit =
Expression.MemberInit(
Expression.New(tKV),
Expression.Bind(miKey, Expression.Constant(1)),
Expression.Bind(miValue, Expression.Constant("Some Value"))
);
或者对于更完整的版本,构造一个完全初始化变量的 lambda 表达式:
public Expression<Func<int, string, KV>> InitKV()
var pK = Expression.Parameter(typeof(int), "k");
var pV = Expression.Parameter(typeof(string), "v");
Type tKV = typeof(KV);
MemberInfo miKey = tKV.GetMember("Key")[0];
MemberInfo miValue = tKV.GetMember("Value")[0];
Expression meminit =
Expression.MemberInit(
Expression.New(tKV),
Expression.Bind(miKey, pK),
Expression.Bind(miValue, pV)
);
return (Expression<Func<int, string, KV>>)Expression.Lambda(meminit, pK, pV);
在你的情况下,那里会有更多的Bind
表达式。
【讨论】:
我尝试改用 MemberInit 但得到了相同的结果:System.Core.dll 中发生了“System.InvalidOperationException”类型的未处理异常附加信息:“System”类型上没有通用方法“GroupJoin” .Linq.Queryable' 与提供的类型参数和参数兼容。如果方法是非泛型的,则不应提供类型参数。 对不起,我显然没有很好地阅读这个问题。我的错,我再看看它。【参考方案2】:答案#2...这次是更实际的答案:P
问题似乎是您传递给 GroupJoin
方法的 lambda 类型等是错误的。
具体来说:
// Create the lambda expression, which represents the complete delegate
Expression<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>> fullResultSelector =
Expression.Lambda<Func<TEntity, TTrans, TransJoin<TEntity, TTrans>>>(fullInitialiser, combinedParam);
...虽然其他一些人看起来也有点可疑,但那可能就是我。
GroupJoin
期望的选择器表达式是 Expression<Func<TEntity, IEnumerable<TTrans>, TransJoin<TEntity, TTrans>>>
类型。它会传入一个TEntity
和一组TTrans
(如IEnumerable<TTrans>
),即使该组中只有一个实例。您的表达式树需要正确处理 IEnumerable<TTrans>
,但目前还没有。
你确定你想要GroupJoin
而不是Join
吗?
我在 LINQPad 中编写了一些代码来测试这个概念。如果您想查看它,请访问>PasteBin<。
顺便说一句,在表达式上使用 LINQPad 的 Dump
扩展将使您全面了解表达式的构造方式。将 lambda 分配给适当的 Expression<Func<....>>
类型的变量,然后调用 Dump
以查看它是如何构造的。帮助找出用法并展示构建它需要做什么。
【讨论】:
> 您的表达式树需要正确处理该 IEnumerableFirstOrDefault
,那么你必须将它从IEnumerable<TTrans>
构建到表达式树中。它不会对你暗示。标准用法也是如此 - 您将得到一个必须处理的 IEnumerable
,而不是单个实例。以上是关于多组连接 lambda 到表达式树的主要内容,如果未能解决你的问题,请参考以下文章
带有语句体的 lambda 表达式无法转换为 nopCommerce 中的表达式树 [重复]