解决了Entity Framework跨数据库查询问题,博客园现代化建设又向前迈进了一步。
在之前的一篇随笔“ 博客园现代化建设——AutoMapper ”中曾谈到,我们所遇到的应用场景是数据库查询返回的字段数少于实体类的属性,而默认情况下Entity Framework根据实体类的属性进行映射的,所以我们改用了AutoMapper。后来, 真见在评论中指出可以在LINQ通过 select new 指定查询返回的字段,Entity Framework会根据返回的字段与实体类属性进行映射。
我们尝试了一下果然可以,但是,在所用的LINQ查询代码中有个地方不够现代化,代码如下:
using (BlogDbContext context = new BlogDbContext())
{
var result = (from e in context.BlogEntries
join t in context.PostTexts
on e.ID equals t.ID
where e.ID == entryId
select new
{
Title = e.Title,
Body = t.Text
})
.ToList()
.Select(e => new BlogEntry() { Title = e.Title, Body = e.Body })
.FirstOrDefault();
}
这是一个根据ID获取文章标题与内容的查询(文章内容存储在一个单独的数据库中,所以之前我们要解决跨数据库查询的问题),从上面的代码一眼就能看出两次的select和一次ToList()显得啰嗦。不是我们想这么啰嗦,实在是情非得已。理想中的查询当然是下面这样的:
using (BlogDbContext context = new BlogDbContext())
{
var result = (from e in context.BlogEntries
join t in context.PostTexts
on e.ID equals t.ID
where e.ID == entryId
select new BlogEntry()
{
Title = e.Title,
Body = t.Text
})
.FirstOrDefault();
}
但是,如果这样,将面临残酷的现实:
System.NotSupportedException :
The entity or complex type 'BlogServer.Data.Provider.BlogEntry'
cannot be constructed in a LINQ to Entities query.
at System.Data.Objects.ELinq.ExpressionConverter.CheckInitializerType(Type type)
如果美好的理想遇到残酷的现实就放弃,你永远得不到成长;你要与现实斗争到底,即使理想破灭,也要找到破灭的原因,也许下一个理想就成为现实。
而这篇文章写的就是我们与这个残酷现实斗争的过程。
上面的错误信息最让人抓狂的是:明明类型是BlogServer.Entities.BlogEntry,提示的却是 BlogServer.Data.Provider.BlogEntry,根本不存在这个类型,只是查询代码所在类型的命名空间是 BlogServer.Data.Provider。而且代码是编译通过的,不知Entity Framework怎么整出这么个异类。
错误信息已经提示我们异常发生在 System.Data.Objects.ELinq.ExpressionConverter.CheckInitializerType(Type type)中,所以要去Entity Framework的源代码中看个究竟。哎,又要借助Reflector挖地道(强烈要求微软开放Entity Framework的源代码)。
挖好地道,直达CheckInitializerType(Type type),拍了张照片:
根据我们的猜测,异常是在上图中的高亮部分抛出的(毕竟是地道,找东西很不方便,目前还没找出证实这个猜测的确凿代码,请谅解)。
从上面的代码可以看出,异常是在BuiltInTypeKind.EntityType或BuiltInTypeKind.ComplexType 的条件下触发的。由于出错信息中提到了“complex type”,所以,一开始我们将焦点锁定在这个条件上,在Entity Framework的源代码中转悠了半天,也没找到在哪设置了这个值。
后来发现,抛出异常信息时并没有传递BuiltInTypeKind,也就是说“complex type”与BuiltInTypeKind.ComplexType没有关系。于是,我们把目光转向了 BuiltInTypeKind.EntityType,"EntityType"这个名称让我们产生了联想:我们在BlogDbContext中注册了 BlogEntry这个类型。代码如下:
public class BlogDbContext : DbContext
{
public DbSet<BlogEntry> BlogEntries { get; set; }
}
也许问题与这个有关联?在毫无头绪时,发现这个线索,让人很兴奋。
于是,我们新建了一个与BlogEntry具有同样属性的BlogEntryClone,将LINQ查询代码改为:
using (BlogDbContext context = new BlogDbContext())
{
var result = (from e in context.BlogEntries
join t in context.PostTexts
on e.ID equals t.ID
where e.ID == entryId
select new BlogEntryClone()
{
Title = e.Title,
Body = t.Text
})
.FirstOrDefault();
}
让人惊喜的是:运行成功,未出现异常。猜测被证实,果然与BlogDbContext中注册了BlogEntry有关。
惊喜之后是困惑,什么要有这个限制?而且,BlogEntryClone与BlogEntry不能有任何联系,继承或聚合都不行。
...
这个问题,以及之前的跨数据库查询问题让我们浮想联翩。微软在设计Entity Framework时,似乎进行了有点脱离实际需求的假设:假设使用Entity Framework时,用不到跨数据库查询;假设使用Entity Framework时,很少需要指定查询返回的字段...
Entity Framework给我们带来了美好的希望,希望微软不要让大家的希望落空,希望Entity Framework能像ASP.NET MVC一样开放(开源),希望Entity Framework能像ASP.NET MVC一样干得漂亮...