Linq To Sql(以下简称LS)从一降世似乎就是个问题宝宝。批更新问题, Like问题,RTM之前的"BUG"(select new不能显式创建实体)等等接踵而至,很多时候我们不得不回到SqlCommand去来"扩展"LS,不巧的是,LS留给程序员的灵活性很有限,我们的Extension总是有不尽完美的地方,在自我扩展的同时我更多的是期待Entity Framework正式Release时候能解决这些问题,至少是能够带给我们更多的灵活性。今天我要说的是LS中的继承问题,先看实体关系:
图中已经去掉了多余的字段,仅仅留下关键字段,简要说明一下:Order实体和Supplier实体对应数据库中的表,而OrderInfo实体很明显是连接了两个实体的“视图”。这样的应用场景很常见,我可能需要OrderInfo去填充一个Grid,而又需要在OrderInfo被修改之后提交回数据库, 很显然OrderInfo必须是一个实际类型而非匿名类型。有人会问为什么不用视图去生成OrderInfo实体,原因很简单,视图是无法提交修改的,为了让能够OrderInfo直接提交回数据库更好的做法是让他继承于Order,于是乎,很自然的想到OrderInfo的代码是这样:
那么查询可以是这样的:
同时去掉OrderInfo类上的TableAttribute,注意,上面的Order类代码是放在LS设计器生成的代码之外的运用了局部类特性,不好意思这样的代码过不去,LS的继承方式必须要有个叫做鉴别器列的东西,那么我们还要向Order类添加一个属性:
似乎很阴险,骗过LS,让他认为Type列示个自动生成列,这样提交时候就不会向这个列插入数据了,同时把AutoSync属性置为AutoSync.Never,这样做是因为LS在做插入或者更新操作时候会把这个列的值同步到实体,算是LS应对并发的一点自动化操作吧,如果他同步属性值那同样会由于数据库中没这个列而失败,因此我们禁止这样的同步。
好了,现在我们再用JeffreyZhao的那个扩展去查询,在这里借花献佛了(很感谢老赵提供了这个扩展),展示一个那个扩展是怎么用的:
非常抱歉,这个扩展还是失败了,原因是鉴别器列并没有在查询结果中,因此使用了默认类型Order来做Translate(默认类型设置在[System.Data.Linq.Mapping.InheritanceMapping(Code = "Order", Type = typeof(Order), IsDefault = true)]),最后导致了无法将Order强制转换为OrderInfo的异常。如何应对?两个方法:方法一,把Type = typeof(OrderInfo)那个InheritanceMapping特性设为IsDefault = true, 这样其实治标不治本,当你有第二个类继承于Order时候怎么办?方法二,想办法在查询结果中包含鉴别器列的值,很自然的想到查询代码select时候:
SELECT [t0].[OrderId], [t0].[SupplierId], [t1].[SupplierName]
FROM [Order] AS [t0]
INNER JOIN [Supplier] AS [t1] ON [t0].[SupplierId] = [t1].[SupplierId]
也就是说,查询结果中还是没有Type列,可以看出LS在处理常量时候是在创建对象时候赋值的,而不是在SQL端查询,在没有更好的做法时候我只能回到SqlCommand去处理这个强行查询这个常量个,我们的目标是让SQL变成这个样子:
SELECT [t0].[OrderId], [t0].[SupplierId], [t1].[SupplierName], 'OrderInfo' AS [Type]
FROM [Order] AS [t0]
INNER JOIN [Supplier] AS [t1] ON [t0].[SupplierId] = [t1].[SupplierId]
我看过JeffreyZhao的那个扩展,他提供一个开关了控制查询锁,我们对其进一步扩展,也提供一个开关来控制是否生成鉴别器列,在做这个扩展之前我写了这么几个扩展方法使用:
我在那个扩展中添加一个方法(很多代码都是自己直接修改了JeffreyZhao的代码,主要是懒得写了,特此声明,我不是郭敬明,剽窃我是会道歉的):
完成很扩展(有的代码我有所修改,我不太理解为什么JeffreyZhao的代码中要在方法外部关闭数据库连接,为了降低打开连接的开销吗?可是有连接池啊,疑惑中。。。。):
有了这个扩展我们可以很方便的查询了:
图中已经去掉了多余的字段,仅仅留下关键字段,简要说明一下:Order实体和Supplier实体对应数据库中的表,而OrderInfo实体很明显是连接了两个实体的“视图”。这样的应用场景很常见,我可能需要OrderInfo去填充一个Grid,而又需要在OrderInfo被修改之后提交回数据库, 很显然OrderInfo必须是一个实际类型而非匿名类型。有人会问为什么不用视图去生成OrderInfo实体,原因很简单,视图是无法提交修改的,为了让能够OrderInfo直接提交回数据库更好的做法是让他继承于Order,于是乎,很自然的想到OrderInfo的代码是这样:
[Table(Name = "dbo.Order")]
public class OrderInfo : Order
{
private string _SupplierName;
public OrderInfo()
{
}
public string SupplierName
{
get
{
return this._SupplierName;
}
set
{
if ((this._SupplierName != value))
{
this._SupplierName = value;
}
}
}
}
public class OrderInfo : Order
{
private string _SupplierName;
public OrderInfo()
{
}
public string SupplierName
{
get
{
return this._SupplierName;
}
set
{
if ((this._SupplierName != value))
{
this._SupplierName = value;
}
}
}
}
那么查询可以是这样的:
using (DataClasses1DataContext context = new DataClasses1DataContext())
{
OrderInfo[] infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new OrderInfo
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return infos;
}
不好意思,RTM之后这个语句运行时候会出错,具体的可以参看JeffreyZhao的文章,里面给了一个扩展可以绕过这先限制,但是遗憾的是在这个问题上那个扩展一样失败,原因是OrderInfo从基类(Order)继承下来的属性不属于OrderInfo的映射,我很自然的想到用LS继承方式,那么代码可以这样:{
OrderInfo[] infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new OrderInfo
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return infos;
}
[System.Data.Linq.Mapping.InheritanceMapping(Code = "Order", Type = typeof(Order), IsDefault = true)]
[System.Data.Linq.Mapping.InheritanceMapping(Code = "OrderInfo", Type = typeof(OrderInfo))]
public partial class Order
{
[Column(IsDiscriminator = true, IsDbGenerated = true, AutoSync = AutoSync.Never)]
public string Type { get; set; }
}
[System.Data.Linq.Mapping.InheritanceMapping(Code = "OrderInfo", Type = typeof(OrderInfo))]
public partial class Order
{
[Column(IsDiscriminator = true, IsDbGenerated = true, AutoSync = AutoSync.Never)]
public string Type { get; set; }
}
同时去掉OrderInfo类上的TableAttribute,注意,上面的Order类代码是放在LS设计器生成的代码之外的运用了局部类特性,不好意思这样的代码过不去,LS的继承方式必须要有个叫做鉴别器列的东西,那么我们还要向Order类添加一个属性:
[Column(IsDiscriminator = true)]
public string Type { get; set; }
很显然,这样的属性必定导致我们提交数据失败,因为数据库中根本没有这个列,那么我们在做一点手脚:public string Type { get; set; }
[Column(IsDiscriminator = true, IsDbGenerated = true, AutoSync = AutoSync.Never)]
public string Type { get; set; }
public string Type { get; set; }
似乎很阴险,骗过LS,让他认为Type列示个自动生成列,这样提交时候就不会向这个列插入数据了,同时把AutoSync属性置为AutoSync.Never,这样做是因为LS在做插入或者更新操作时候会把这个列的值同步到实体,算是LS应对并发的一点自动化操作吧,如果他同步属性值那同样会由于数据库中没这个列而失败,因此我们禁止这样的同步。
好了,现在我们再用JeffreyZhao的那个扩展去查询,在这里借花献佛了(很感谢老赵提供了这个扩展),展示一个那个扩展是怎么用的:
using (DataClasses1DataContext context = new DataClasses1DataContext())
{
IQueryable infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return context.ExecuteQuery<OrderInfo>(infos);
}
{
IQueryable infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return context.ExecuteQuery<OrderInfo>(infos);
}
非常抱歉,这个扩展还是失败了,原因是鉴别器列并没有在查询结果中,因此使用了默认类型Order来做Translate(默认类型设置在[System.Data.Linq.Mapping.InheritanceMapping(Code = "Order", Type = typeof(Order), IsDefault = true)]),最后导致了无法将Order强制转换为OrderInfo的异常。如何应对?两个方法:方法一,把Type = typeof(OrderInfo)那个InheritanceMapping特性设为IsDefault = true, 这样其实治标不治本,当你有第二个类继承于Order时候怎么办?方法二,想办法在查询结果中包含鉴别器列的值,很自然的想到查询代码select时候:
select new
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName,
Type = "OrderInfo"
};
不好意思,非常不好意思,我们追踪这个代码获得的Sql是这样的:{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName,
Type = "OrderInfo"
};
SELECT [t0].[OrderId], [t0].[SupplierId], [t1].[SupplierName]
FROM [Order] AS [t0]
INNER JOIN [Supplier] AS [t1] ON [t0].[SupplierId] = [t1].[SupplierId]
也就是说,查询结果中还是没有Type列,可以看出LS在处理常量时候是在创建对象时候赋值的,而不是在SQL端查询,在没有更好的做法时候我只能回到SqlCommand去处理这个强行查询这个常量个,我们的目标是让SQL变成这个样子:
SELECT [t0].[OrderId], [t0].[SupplierId], [t1].[SupplierName], 'OrderInfo' AS [Type]
FROM [Order] AS [t0]
INNER JOIN [Supplier] AS [t1] ON [t0].[SupplierId] = [t1].[SupplierId]
我看过JeffreyZhao的那个扩展,他提供一个开关了控制查询锁,我们对其进一步扩展,也提供一个开关来控制是否生成鉴别器列,在做这个扩展之前我写了这么几个扩展方法使用:
TypeExtension
我在那个扩展中添加一个方法(很多代码都是自己直接修改了JeffreyZhao的代码,主要是懒得写了,特此声明,我不是郭敬明,剽窃我是会道歉的):
private static Regex m_fieldResgex = new Regex(@"\sFROM\s", RegexOptions.IgnoreCase);
private static void AddDiscriminatorColum<T>(DbCommand cmd)
{
string cmdText = cmd.CommandText;
IEnumerable<Match> matches =
m_fieldResgex.Matches(cmdText).Cast<Match>().OrderByDescending(m => m.Index);
foreach (Match m in matches)
{
int splitIndex = m.Index;
cmdText =
cmdText.Substring(0, splitIndex) + GetDiscriminatorColumSelectString<T>() +
cmdText.Substring(splitIndex);
}
cmd.CommandText = cmdText;
}
private static string GetDiscriminatorColumSelectString<T>()
{
Type type = typeof(T);
Type topType = type.FindTopEntityType();
string value = topType.GetDiscriminatorCode(type).ToString();
string columnName = topType.GetDiscriminatorColumnName();
if (!String.IsNullOrEmpty(value) && !String.IsNullOrEmpty(columnName))
{
return String.Format(@", '{0}' AS [{1}] ", value, columnName);
}
return String.Empty;
}
private static void AddDiscriminatorColum<T>(DbCommand cmd)
{
string cmdText = cmd.CommandText;
IEnumerable<Match> matches =
m_fieldResgex.Matches(cmdText).Cast<Match>().OrderByDescending(m => m.Index);
foreach (Match m in matches)
{
int splitIndex = m.Index;
cmdText =
cmdText.Substring(0, splitIndex) + GetDiscriminatorColumSelectString<T>() +
cmdText.Substring(splitIndex);
}
cmd.CommandText = cmdText;
}
private static string GetDiscriminatorColumSelectString<T>()
{
Type type = typeof(T);
Type topType = type.FindTopEntityType();
string value = topType.GetDiscriminatorCode(type).ToString();
string columnName = topType.GetDiscriminatorColumnName();
if (!String.IsNullOrEmpty(value) && !String.IsNullOrEmpty(columnName))
{
return String.Format(@", '{0}' AS [{1}] ", value, columnName);
}
return String.Empty;
}
完成很扩展(有的代码我有所修改,我不太理解为什么JeffreyZhao的代码中要在方法外部关闭数据库连接,为了降低打开连接的开销吗?可是有连接池啊,疑惑中。。。。):
public static List<T> ExecuteQuery<T>(this DataContext dataContext, IQueryable query, bool withNoLock, bool generateDiscriminateColumn)
{
string discriminatorCode = null;
Type type = typeof(T);
DbCommand command = dataContext.GetCommand(query, withNoLock);
if (generateDiscriminateColumn)
{
AddDiscriminatorColum<T>(command);
}
try
{
command.Connection.Open();
using (DbDataReader reader = command.ExecuteReader())
{
return dataContext.Translate<T>(reader).ToList();
}
}
finally
{
if (command != null)
{
command.Connection.Close();
}
command.Dispose();
}
}
{
string discriminatorCode = null;
Type type = typeof(T);
DbCommand command = dataContext.GetCommand(query, withNoLock);
if (generateDiscriminateColumn)
{
AddDiscriminatorColum<T>(command);
}
try
{
command.Connection.Open();
using (DbDataReader reader = command.ExecuteReader())
{
return dataContext.Translate<T>(reader).ToList();
}
}
finally
{
if (command != null)
{
command.Connection.Close();
}
command.Dispose();
}
}
有了这个扩展我们可以很方便的查询了:
using (DataClasses1DataContext context = new DataClasses1DataContext())
{
IQueryable infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return context.ExecuteQuery<OrderInfo>(infos, true, true);
}
一切似乎很顺利,不好意思,非常非常不好意思,还是有问题,这个查询得到的OrderInfo.SupplierName属性总是为空,对于这个问题我现在也不是很明白,这个扩展的关键是DataContext.Translate方法,这个方法是完成查询结果到Entity的映射,我们看看这个方法的算法, MSDN上是这样写的:{
IQueryable infos = from o in context.Orders
join s in context.Suppliers on o.SupplierId equals s.SupplierId
select new
{
OrderId = o.OrderId,
SupplierId = o.SupplierId,
SupplierName = s.SupplierName
};
return context.ExecuteQuery<OrderInfo>(infos, true, true);
}
-
使查询结果中的列与对象中的字段和属性相匹配的算法如下所示:
-
如果字段或属性映射到特定列名称,则结果集中应包含该列名称。
-
如果未映射字段或属性,则结果集中应包含其名称与该字段或属性相同的列。
通过先查找区分大小写的匹配来执行比较。如果未找到匹配项,则会继续搜索不区分大小写的匹配项。
-
-
如果同时满足下列所有条件,则该查询应当返回(除延迟加载的对象外的)对象的所有跟踪的字段和属性:
-
T 是由 DataContext 显式跟踪的实体。
-
ObjectTrackingEnabled 为 true。
-
实体具有主键。
-
我标注了红色的两个地方个人认为是对立的,我的理解是如果是DataContext追踪的实体则只映射对象追踪的字段和属性,如果不是DataContext追踪的实体则会映射追踪的和不追踪的属性和字段。
解决之道是在OrderInfo.SupplierName属性上加上ColumnAttribute特性,同时处于更新时候的需要,一样要欺骗LS:
[Column(Storage="_SupplierName", IsDbGenerated = true, AutoSync = AutoSync.Never)]
public string SupplierName
{
get
{
return this._SupplierName;
}
set
{
if ((this._SupplierName != value))
{
this._SupplierName = value;
}
}
}
public string SupplierName
{
get
{
return this._SupplierName;
}
set
{
if ((this._SupplierName != value))
{
this._SupplierName = value;
}
}
}
再次执行扩展,一切顺利,我们也就可以这样插入数据了:
OrderInfo info = new OrderInfo();
using (DataClasses1DataContext context = new DataClasses1DataContext())
{
context.Orders.InsertOnSubmit(info);
context.SubmitChanges();
}
using (DataClasses1DataContext context = new DataClasses1DataContext())
{
context.Orders.InsertOnSubmit(info);
context.SubmitChanges();
}
写到这里我还是要感谢老赵无私的提供他的LS扩展思路和CODE,同时也对老赵在LS方面的独到见解感到钦佩和赞赏。