如果允许在UI层直接访问Linq to Sql的DataContext,可以省去很多问题,譬如在处理多表join的时候,我们使用var来定义L2S查询,让IDE自动推断变量的具体类型(IQueryable<匿名类型>),并提供友好的智能提示;而且可以充分应用L2S的延迟加载特性,来进行动态查询。但如果我们希望将业务逻辑放在一个独立的层中(譬如封装在远程的WCF应用中),又希望在逻辑层应用Linq to sql,则情况就比较复杂了;由于我们只能使用var(IQueryable<匿名类型>),而var只能定义方法(Method)范围中声明的变量,出了方法(Method)之后IDE就不认得它了;在这种对IQueryable<匿名类型>一无所知的情况下,又希望能在开发时也能应用上IDE的智能感应,我们该怎么定义层之间交互的数据传输载体呢?又如何对它进行动态查询呢?
内容比较多,分上下两篇,上篇介绍查询返回自定义实体,下篇介绍动态查询。
下面来看一个示例(以NorthWind数据库为示例),现在我们要在界面上展示某个用户什么时间订购了哪些产品。
如果允许在UI层直接访问DataContext,我们可以这样来写:
1: using (NorthWindDataContext context = new NorthWindDataContext())
2: {
3: var query0 = from C in context.Customers
4: join O in context.Orders
5: on C.CustomerID equals O.CustomerID
6: join OD in context.Order_Details
7: on O.OrderID equals OD.OrderID
8: join P in context.Products
9: on OD.ProductID equals P.ProductID
10: select new
11: {
12: C.CustomerID,
13: C.CompanyName,
14: C.ContactName,
15: C.Address,
16: O.OrderDate,
17: P.ProductName
18: };
19: gridView.DataSource = query0.ToList();
20: gridView.DataBind();
21: }
这里只查询需要显示的列,避免返回不必要的列。查询返回的是一个泛型匿名对象集合,由于绑定操作与查询操作在同一个方法内,所以IDE会自动帮忙推断var的对象类型。但如果要将查询逻辑封装在远程的WCF中,我们该用啥作为层之间交互的数据传输载体呢?List<???>,里面的“???”该是啥呢?
以下是我尝试过的几种方案和走过的弯路。
从上面的代码中可以看到,我们需要返回的属性信息主要来源于Customers实体,下面来尝试下能否在该实体的定义中直接附加字段OrderDate和ProductName:
1: partial class Customers
2: {
3: public DateTime OrderDate {get;set;}
4: public string ProductName { get; set; }
5: }
然后这样来写查询,看看能不能欺骗L2S来自动匹配这新增的两个属性:
1: public List<Customers> GetOrderInfo(string customerID)
2: {
3: using (NorthWindDataContext context = new NorthWindDataContext())
4: {
5: var query1 = from C in context.Customers
6: join O in context.Orders
7: on C.CustomerID equals O.CustomerID
8: join OD in context.Order_Details
9: on O.OrderID equals OD.OrderID
10: join P in context.Products
11: on OD.ProductID equals P.ProductID
12: where C.CustomerID == customerID
13: select C; //直接返回实体
14:
15: //或者这样
16: var query2 = from C in context.Customers
17: join O in context.Orders
18: on C.CustomerID equals O.CustomerID
19: join OD in context.Order_Details
20: on O.OrderID equals OD.OrderID
21: join P in context.Products
22: on OD.ProductID equals P.ProductID
23: where C.CustomerID == customerID
24: select new Customers //显示构造实体
构造实体
25: {
26: CustomerID = C.CustomerID,
27: CompanyName = C.CompanyName,
28: ContactName = C.ContactName,
29: Address = C.Address,
30: OrderDate = O.OrderDate,
31: ProductName = P.ProductName
32: };
33: return query1.ToList(); //query2.ToList()
34: }
35: }
很遗憾的是,query1查询执行的结果,没有取得我们需要的数据:
而query2也抛出了NotSupportedException:不允许在查询中显式构造实体类型“TestLINQ.Customers”。
看来,这种方法行不通。
在老赵的这篇文章中:《在LINQ to SQL中使用Translate方法以及修改查询用SQL》,里面提出了一种方法来来砍掉那些不需要加载的信息,且可以继续使用LINQ to SQL进行查询。
这里借鉴下里面的思路,看看在增加属性的情况下,结果会怎样:
1: public List<Customers> GetOrderInfo(string customerID)
2: {
3: using (NorthWindDataContext context = new NorthWindDataContext())
4: {
5: var query3 = query0;
6: return context.ExecuteQuery<Customers>(query);
7: }
8: }
说明:
(1) 这里的Customers类型定义,继续用上一节中的对实体类的扩展;
(2) DataContext.ExcuteQuery<T>(IQuery query)方法,使用的老赵的DataContext扩展;
(3) 为避免L2S查询占用太多的版面,前面对每个查询都进行了编号,query0, query1, query2….,下面如果需要用到同样的查询时,直接引用前面的查询,以节省版面和突出重点。
很遗憾的是,这次希望又落空了。返回的结果中,OrderDate和ProductName依然为空。
老赵只提供了砍掉不需要的字段的方法,把增加字段的方法自己留着了/:)
另外补充一点,这里对老赵提供的方法做了一点儿改进:如果调用OpenConnection时打开了新的连接,则需要在用完后关闭该连接,下面是代码:
1: public static List<T> ExecuteQuery<T>(this DataContext dataContext, IQueryable query)
2: {
3: using (DbCommand command = dataContext.GetCommand(query))
4: {
5: bool openNewConnecion = false;
6: try
7: {
8: openNewConnecion = dataContext.OpenConnection();
9: using (DbDataReader reader = command.ExecuteReader())
10: {
11: return dataContext.Translate<T>(reader).ToList();
12: }
13: }
14: finally
15: {
16: if (openNewConnecion) //如果打开了新的连接,则需要手动Close
17: dataContext.Connection.Close();
18: }
19: }
20: }
21:
22: /// <summary>
23: /// 打开连接
24: /// </summary>
25: /// <param name="dataContext"></param>
26: /// <returns>是否打开了新的连接(这个返回值可能容易让人误解,汗...)</returns>
27: private static bool OpenConnection(this DataContext dataContext)
28: {
29: if (dataContext.Connection.State == ConnectionState.Closed)
30: {
31: dataContext.Connection.Open();
32: return true;
33: }
34: return false;
35: }
使用DataContext自带的ExcuteQuery<T>方法:
1: public List<Customers> GetOrderInfo(string customerID)
2: {
3: using (NorthWindDataContext context = new NorthWindDataContext())
4: {
5: string sql = @"SELECT C.CustomerID, C.CompanyName, C.ContactName, C.[Address], O.OrderDate, P.ProductName
6: dbo.Customers AS C
7: dbo.Orders AS O
8: ON O.CustomerID = C.CustomerID
9: dbo.[Order Details] AS OD
10: ON OD.OrderID = O.OrderID
11: dbo.Products AS P
12: ON P.ProductID = OD.ProductID
13: E C.CustomerID={0}";
14: return context.ExecuteQuery<Customers>(sql, customerID).ToList();
15: }
16: }
结果跟第二节中的结果相同,又失败了……
补充,MSDN上关于Translate和ExcuteQuery对查询结果进行转换的描述如下:
否则会引发异常。 |
我愣是看了好多遍,还是没有搞明白,为啥将结果集转换到对象集合时L2S把我增加的字段给抛弃了……
既然不让我在L2S生成的默认实体上直接进行扩展,那我可以派生一个实体并添加我们需要的字段吗?
1: public class CustomerExt : Customers
2: {
3: public DateTime? OrderDate {get;set;}
4: public string ProductName { get; set; }
5: }
然后在业务逻辑层里面这样写:
1: public List<CustomerExt> GetOrderInfo(string customerID)
2: {
3: using (NorthWindDataContext context = new NorthWindDataContext())
4: {
5: var query4 = query0
6: return context.ExecuteQuery<CustomerExt>(query).ToList();
7: }
8: }
遗憾的是,程序执行到dataContext.Translate<T>(reader).ToList()时,又出错了,抛出了InvalidOperationException异常:
未处理 System.InvalidOperationException
Message="类型为“TestLINQ.Customers”的数据成员“System.String CustomerID”不是类型“CustomerExt”的映射的一部分。该成员是否位于继承层次结构根节点的上方?"
Source="System.Data.Linq"
StackTrace:
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.GetRequiredInheritanceDataMember(MetaType type, MemberInfo mi)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.AccessMember(SqlMember m, SqlExpression expo)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.VisitMember(SqlMember m)
在 System.Data.Linq.SqlClient.SqlVisitor.Visit(SqlNode node)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.VisitExpression(SqlExpression expr)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.VisitNew(SqlNew sox)
在 System.Data.Linq.SqlClient.SqlVisitor.Visit(SqlNode node)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.VisitExpression(SqlExpression expr)
在 System.Data.Linq.SqlClient.SqlVisitor.VisitUserQuery(SqlUserQuery suq)
在 System.Data.Linq.SqlClient.SqlBinder.Visitor.VisitUserQuery(SqlUserQuery suq)
在 System.Data.Linq.SqlClient.SqlVisitor.Visit(SqlNode node)
在 System.Data.Linq.SqlClient.SqlBinder.Bind(SqlNode node)
在 System.Data.Linq.SqlClient.SqlProvider.BuildQuery(ResultShape resultShape, Type resultType, SqlNode node, ReadOnlyCollection`1 parentParameters, SqlNodeAnnotations annotations)
在 System.Data.Linq.SqlClient.SqlProvider.GetDefaultFactory(MetaType rowType)
在 System.Data.Linq.SqlClient.SqlProvider.System.Data.Linq.Provider.IProvider.Translate(Type elementType, DbDataReader reader)
在 System.Data.Linq.DataContext.Translate(Type elementType, DbDataReader reader)
在 System.Data.Linq.DataContext.Translate[TResult](DbDataReader reader)
在 TestLINQ.DataContextExtensions.ExecuteQuery[T](DataContext dataContext, DbCommand command) 位置 D:\04.Other\WinForm\TestLINQ\DataContextExtensions.cs:行号 74
在 TestLINQ.DataContextExtensions.ExecuteQuery[T](DataContext dataContext, IQueryable query, Boolean withNoLock) 位置 D:\04.Other\WinForm\TestLINQ\DataContextExtensions.cs:行号 53
在 TestLINQ.DataContextExtensions.ExecuteQuery[T](DataContext dataContext, IQueryable query) 位置 D:\04.Other\WinForm\TestLINQ\DataContextExtensions.cs:行号 28
在 TestLINQ.Program.GetOrderInfo(String customerID) 位置 D:\04.Other\WinForm\TestLINQ\Class1.cs:行号 49
在 TestLINQ.Program.Main(String[] args) 位置 D:\04.Other\WinForm\TestLINQ\Class1.cs:行号 21
在 System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
在 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
在 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
在 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
在 System.Threading.ThreadHelper.ThreadStart()
InnerException:
回过头来看看L2S中的继承,MSDN说法如下:若要在 LINQ 中执行继承映射,您必须在继承层次结构的根类中指定属性 (Attribute) 和属性 (Attribute) 的属性 (Property)。(FROM MSDN: 映射继承层次结构 (LINQ to SQL))
看得我有点儿晕晕的....如果我不想修改L2S帮我生成的类型定义文件,则需要通过partial类对默认生成的Customers进行扩展:扩展一个属性作为鉴别器值?
好像挺绕的,我最终还是没有尝试成功……
上面啰嗦了这么多废话,是我使用L2S过程中走过的一些弯路,列出来供大家参考,避免重蹈我的覆辙。
---------------------------------------------------------------------------------------------------------------
--------------------------我是华丽的分割线(happyhippy.cnblogs.com)-------------------------------
---------------------------------------------------------------------------------------------------------------
在上面一节尝试使用继承时,查看错误堆栈信息,最后定位到GetRequiredInheritanceDataMember这里,这是在访问基类成员时出错了。于是我起了个邪恶的念头,把基类抛弃掉,显式再定义一个实体看看:
1: public class CustomerOrderDetial
2: {
3: public string CustomerID { get; set; }
4: public string CompanyName { get; set; }
5: public string ContactName { get; set; }
6: public string Address { get; set; }
7: public DateTime? OrderDate { get; set; }
8: public string ProductName { get; set; }
9: }
10:
11: public List<CustomerOrderDetial> GetOrderInfo(string customerID)
12: {
13: using (NorthWindDataContext context = new NorthWindDataContext())
14: {
15: var query5 = query0
16: return context.ExecuteQuery<CustomerOrderDetial>(query5).ToList();
17: }
18: }
这次运行通过了,而且得到了我们想要的结果,Congratulations!
但是,这样操作的话,每次我们都要去手工编写代码,将我们需要的字段封装成一个实体类型。
结合上面第3节中的结论,我推测Translate和ExcuteQuery是按照下列逻辑来将结果集转换成对象集合的:
1: if(实体是由Table影射的实体)
2: {
3: 转换时,只匹配标记为[Column]的属性
4: }
5: else //显式自定义实体(参考下面第4节)
6: {
7: 转换时,根据属性名与结果集中的列名进行匹配
8: }
另一种方法是使用视图、或存储过程、或自定义函数,让L2S设计器或者SqlMeta工具将视图映射成实体,或生成调用存储过程和自定义函数的代码。
可以参考MSDN:存储过程 (LINQ to SQL)。使用自定义函数过程与存储过程差不错,使用视图的过程与表差不多,具体可以看MSDN中介绍,及L2S生成的源代码,这里就不啰嗦了。
然而,视图、存储过程、自定义函数也不是万金油。就拿本文的例子来说,我们的应用场景是“查询客户什么时间订了哪些产品”,于是我们定义了一个视图来关联相关的四张表;但一个应用系统中,往往会有很多场景;各种场景相互之间很相似,但又有不同,譬如“查询客户什么时间订了哪些公司生产的哪些产品”、“查询客户什么时间订了哪些雇员销售的哪些产品”,我们又该怎么处理呢?为每个场景定制一个视图?还是做一个“聪明”的大视图,把所有关联的表都join起来?
使用前者的结果可能会是,试图的数量呈爆炸式增长;
使用后者的结果可能会是:聪明反被聪明误,性能不是一般地差。
前面的两种方法虽然都可行,但用起来还是有点儿麻烦,能不能简单一点儿呢?
在使用LINQ之前,我们经常使用Ado.Net从数据库中取得一个数据集(DataSet或者DataTable),然后再根据列名称与对象的属性名进行匹配,将数据集转换成对象集合List<T>。在本节中,我将参考这个思路,自定义一个对象转换器。
LINQ中,有一个扩展方法IEnumerable.Cast<TResult>,实现了从IEnumerable到IEnumerable<TResult>的转换,里面实现的是遍历源集合,然后将里面的元素进强制类型转换TResult类型,最后返回IEnumerable<TResult>。但这里,我们要实现的是,将IEnumerable<匿名类型>转换成IEnumerable<命名类型>,使用该转换器的代码示例如下图所示:
下面是执行结果(其中CustomerExt使用第4节中的实体定义,继承自Customers):
使用起来还算比较清爽;当然,也有不足之处,性能怎样是一个考虑点,还有就是如上面的运行结果截图,一些被我们坎掉的字段也会显示出来;虽然这些额外字段的值都为空,但考虑下列情况:UI层取得的结果是List<CustomerExt>,但他怎么知道CustomerExt中哪些字段可以用,哪些字段被阉割了呢?答案是:源代码前面没有秘密,只有看底层的源代码了-.-
下面来看下这个对象转换器的源代码:
1: public static class ObjectConverter
2: {
3: private class CommonProperty
4: {
5: public PropertyInfo SourceProperty { get; set; }
6: public PropertyInfo TargetProperty { get; set; }
7: }
8:
9: public static List<TResult> ConvertTo<TResult>(this IEnumerable source)
10: where TResult : new()
11: {
12: if (source == null) //啥都不用干
13: return null;
14:
15: if (source is IEnumerable<TResult>)
16: return source.Cast<TResult>().ToList();//源类型于目标类型一致,可以直接转换
17:
18: List<TResult> result = new List<TResult>();
19: bool hasGetElementType = false;
20: IEnumerable<CommonProperty> commonProperties = null; //公共属性(按属性名称进行匹配)
21:
22: foreach (var s in source)
23: {
24: if (!hasGetElementType) //访问第一个元素时,取得属性对应关系;后续的元素就不用再重新计算了
25: {
26: if (s is TResult) //如果源类型是目标类型的子类,可以直接Cast<T>扩展方法
27: {
28: return source.Cast<TResult>().ToList();
29: }
30: commonProperties = GetCommonProperties(s.GetType(), typeof(TResult));
31: hasGetElementType = true;
32: }
33:
34: TResult t = new TResult();
35: foreach (CommonProperty commonProperty in commonProperties) //逐个属性拷贝
36: {
37: object value = commonProperty.SourceProperty.GetValue(s, null);
38: commonProperty.TargetProperty.SetValue(t, value, null);
39: }
40: result.Add(t);
41: }
42:
43: return result;
44: }
45:
46: private static IEnumerable<CommonProperty> GetCommonProperties(Type sourceType, Type targetType)
47: {
48: PropertyInfo[] sourceTypeProperties = sourceType.GetProperties();//获取源对象所有属性
49: PropertyInfo[] targetTypeProperties = targetType.GetProperties(); //获取目标对象所有属性
50: return from SP in sourceTypeProperties
51: join TP in targetTypeProperties
52: on SP.Name.ToLower() equals TP.Name.ToLower() //根据属性名进行对应(不区分大小写)
53: select new CommonProperty
54: {
55: SourceProperty = SP,
56: TargetProperty = TP
57: };
58: }
59: }
源代码前没有秘密,里面就是实现了最简单的转换:将源对象集合中的元素逐个转换成目标对象。
关于这段代码的一点补充说明(下面的源类型和目标类型,是指泛型中的T,而不是IEnumerable<T>):
(1). 如果源类型于目标类型一致,或者源类型是目标类型的子类,则可以不用逐个元素遍历了,直接调用IEnumerable的扩展方法Cast<T>()即可;用Reflector看了下其源代码实现,里面比较绕,不知道性能咋样,暂时不管了,用着先,而且这样很省事儿。
另外List<T>也提供了一个ConvertAll<TOutput>(Converter<T, TOutput> converter)方法,可以自己定义一个对象转换器方法,然后传给Converter<T, TOutput>委托;但这里用不上该方法,原因如下:
a. 看其源代码实现,可以发现其就是遍历集合循环执行Converter委托,这样不便于进行优化(参考下面的第(2)点);
b. 虽然我可以实现一个Converter<T, TOutput>,但在外面该怎样调用呢?因为query的类型是IQueryable<匿名类型>,所以在调用时,我们根本不知道该传啥进去。
(2). 如果不满足(1),则需要逐个元素进行转换。由于在进入foreach(上面代码的第22行)之前,还不知道源类型是什么类型,因此将GetCommonProperties方法放到循环中;但如果源集合中有100个元素,而循环中每次都来执行这个方法,合计执行100次,这样会显得很傻X,因此里面加了点控制,只在处理第一个元素时调用该方法,然后将属性匹配结果缓存下来(使用局部变量commonProperties进行缓存),从而避免每次都做无用功。
(3). 执行返回的结果时List<TResult>,也即是执行此方法时,如果传进来的是IQueryable<T>,则会立即进行计算。
(4). 这里面还有继续优化的余地:如果有100个用户同时在执行这个查询请求,则每个请求里面都在进行执行GetCommonProperties函数,然后各自进行着反射(取得“特定匿名类型”和CustomerExt类型的属性集合)和属性匹配(取得“特定匿名类型”和CustomerExt类型的公共属性)运算,这样又会显得傻X了。对于一个普通的已经部署完毕的应用系统,其中的实体类型定义是恒定的(不考虑动态编译的情况;对于匿名类型,在编译时,编译器会为其创建类型定义),而且类型之间的转换关系也是恒定的,因此我们可以这些信息缓存下来,避免每次请求都执行重复计算。下面是一个最简单的属性缓存器,采用静态变量来保存计算过的信息,直接替换上面的GetCommonProperties方法即可:
1: private static class PropertyCache
2: {
3: private static object syncProperty = new object();
4: private static object syncCommon = new object();
5:
6: private static Dictionary<Type, PropertyInfo[]> PropertyDictionary =
7: new Dictionary<Type, PropertyInfo[]>(); //缓存类型的PropertyInfo数组
8: private static Dictionary<string, IEnumerable<CommonProperty>> CommonPropertyDictionary =
9: new Dictionary<string, IEnumerable<CommonProperty>>(); //缓存两种类型的公共属性对应关系
10:
11: private static PropertyInfo[] GetPropertyInfoArray(Type type)
12: {
13: if (!PropertyCache.PropertyDictionary.ContainsKey(type))
14: {
15: lock (syncProperty)
16: {
17: if (!PropertyCache.PropertyDictionary.ContainsKey(type)) //双重检查
18: {
19: PropertyInfo[] properties = type.GetProperties();
20: PropertyCache.PropertyDictionary.Add(type, properties); //Type是单例的(Singleton),可以直接作为Key
21: }
22: }
23: }
24: return PropertyCache.PropertyDictionary[type];
25: }
26:
27: public static IEnumerable<CommonProperty> GetCommonProperties(Type sourceType, Type targetType)
28: {
29: string key = sourceType.ToString() + targetType.ToString();
30: if (!PropertyCache.CommonPropertyDictionary.ContainsKey(key))
31: {
32: lock (syncCommon)
33: {
34: if (!PropertyCache.CommonPropertyDictionary.ContainsKey(key)) //双重检查
35: {
36: PropertyInfo[] sourceTypeProperties = GetPropertyInfoArray(sourceType);//获取源对象所有属性
37: PropertyInfo[] targetTypeProperties = GetPropertyInfoArray(targetType);//获取目标对象所有属性
38: IEnumerable<CommonProperty> commonProperties = from SP in sourceTypeProperties
39: join TP in targetTypeProperties
40: on SP.Name.ToLower() equals TP.Name.ToLower()
41: select new CommonProperty
42: {
43: SourceProperty = SP,
44: TargetProperty = TP
45: };
46: PropertyCache.CommonPropertyDictionary.Add(key, commonProperties);
47: }
48: }
49: }
50: return PropertyCache.CommonPropertyDictionary[key];
51: }
52: }
上面第7节中,看起来好像解决了文章标题所提出的问题,但这种方式也可能是个陷阱。
其中使用了CustomerExt,其继承自L2S生成的默认实体Customers,这样带来的一个好处就是可以复用Customers中的属性定义,而不必像第5节中一样,重新定义一套。但是从继承的语义上来讲,继承体现的是一种IS-A的关系,因此套用过来的话就是这样:“客户什么时间订购哪些商品”是一个“客户”!???这是啥?幼儿园没毕业吧?打回去重读……
在某些场景下,我们可以应用继承,譬如NorthWind数据库中有张表dbo.Contacts记录用户的联系信息,则我们可以对Customer或者Employee进行扩展,添加联系信息;而对于本文所举的这个例子,继承是被滥用了。当然,本文的重点是Linq to Sql,而不是OO,因此,这里就请各位看官不要追究我的错误了………我先原谅我自己,愿主也原谅我吧,阿弥陀佛。。。
为了将功补过,这里引入一点Entity Framework的东西,下面这个截图来自《Linq in Action》:
在Linq to Sql中,我们只能将表或者视图影射成实体定义,且这种影射是1对1影射。从上图可以看到,在EF中,可以建立一个概念模型,将多个表影射到一个实体定义;于是,整个世界清静了……
我也只是撇了一眼,还没有用过EF,不知道自己理解的对不对;这里只是做个引子,有兴趣的话,各位可以自己研究研究,记得把研究结果分享给我/:)
最有来个总结(由于个人认知的局限性,这些结论可能不一定正确):
可行性 | 缺点 | |
扩展默认实体定义 | 否 | -- |
使用Translate来返回自定义实体 | 否 | -- |
执行TSQL返回自定义实体 | 否 | -- |
继承默认实体定义 | 否 | -- |
显式自定义实体 | 是 | 麻烦,要自己Code,定义新的实体类型 |
使用视图/存储过程/自定义函数 | 是 | 不够灵活,无法为每个应用场景都去订制视图 |
自定义对象转换器 | 是 | 继承关系可能会被滥用;返回的实体集合是个黑盒子,上层可能不知道实体的哪些属性可用,哪些不可用 |
Entity Framework | 貌似可行 | -- |