最近搞Linq To Sql(以下简称LS)上瘾了,越玩越好玩,废话不多说。切入正题批更新和批删除早就被作为LS的软肋被广大程序员们嗤之以鼻,园子里也有人做了些扩展来满足批操作需求,我本来打算直接COPY这些代码在项目中使用的,遗憾的是我没找到批更新的,批删除的有几种,但我都觉得要么太取巧,要又是不完善,我这里也献丑一下,做个批操作扩展,既然大家都做了批删除,我挑个冷门的,批更新吧,其实删除和更新都差不多吧。
我们想要的其实很简单,UPDATE [TABLE] SET ..... WHERE....,尴尬的是这样的语句在LS面前几乎成了奢求,Attach方法勉强可以用用,但是每次重新生成LS实体都要重新设置非条件属性的UpdateCheck = UpdateChek.Never让人无法接受,况且他也就只能做做 WHERE TABLE.XXX = XXX,如果我需要 WHERE TABLE.XXX != XXX或者其他更复杂的条件时这种方法就显得很无力了,扩展,自我扩展,这似乎是我最近在LS娱乐城里玩的最多的游戏了,今天我就要实现这么一个调用Table<TEntity>.Update(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> evaluator):
还是得说说JeffreyZhao(他动作太快了,每次我想写的内容一到他博客里看都是成年旧醋了), 他已经做过批更新的,具体可看这里,遗憾的是如他所说:“放弃了对于复杂表达式树的解析,不支持item.Introduction.Length < 10这种条件”,另有BLACK JACK通过正则式匹配查询语句,可看这里,但是我对那个正则表达式不是很放心,毕竟SQL的变化太多。结合了前面两种做法,我也提出我的思路,我们知道IQueryable的查询语句是可以获得的,那么我们能否在批操作时候直接运用这个SQL呢?答案是肯定的,例如我有这么一个SELECT的SQL(OrderId是主键):
SELECT * from Order WHERE Order.OrderId = 123;
那么可以想办法更新这些查询结果中的记录,SQL语句可以这么写:
UPDATE [Order]
SET [XXX] = xxxx
按照上面的举例,直接调用上面的GetFormatCmdText扩展方法我们将得到如下SQL:
UPDATE [Order]
SET {0}
http://blogs.msdn.com/mattwar/archive/2007/07/31/linq-building-an-iqueryable-provider-part-ii.aspx
里面有ExpressionVisitor类可供使用,代码我就不贴出来了,需要的朋友可浏览上面的链接。通过代码我们可以获取赋值表达式的MemberInitExpression对象,把它转换成SET的SQL语句片段也很简单:
需要注意的是,上面的代码参数中的updateCommand,由于我们的updateCommand其实是通过IQueryable获得的,所以里面可能带了参数,因此我们赋值用的参数必须向这个Command中添加,最后就是语句拼接和参数处理了,也就是完成扩展方法:
有了这个扩展我们就可以这样更新数据了:
这个扩展并非完美无暇,可能有朋友注意了上面代码中GetDbSetString方法有这么一句:
else
{
throw NotSupportedException("not support the method call expression");
}
经发现有时候Expression<Func<TEntity, TEntity>> evaluator表达式的可能产生内嵌的MethodCallExpression的,对于这样的表达式我没有更好的方式来解析,因此放弃了这种情况的处理,例如:
o=> new Order() {Number= o.Number.Number+ " AAA"}
对于这种内联调用这个扩展暂时无能为力,好了,文章完,有了UPDATE的思路DELETE也就可以同样的方式扩展了。
我们想要的其实很简单,UPDATE [TABLE] SET ..... WHERE....,尴尬的是这样的语句在LS面前几乎成了奢求,Attach方法勉强可以用用,但是每次重新生成LS实体都要重新设置非条件属性的UpdateCheck = UpdateChek.Never让人无法接受,况且他也就只能做做 WHERE TABLE.XXX = XXX,如果我需要 WHERE TABLE.XXX != XXX或者其他更复杂的条件时这种方法就显得很无力了,扩展,自我扩展,这似乎是我最近在LS娱乐城里玩的最多的游戏了,今天我就要实现这么一个调用Table<TEntity>.Update(Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> evaluator):
还是得说说JeffreyZhao(他动作太快了,每次我想写的内容一到他博客里看都是成年旧醋了), 他已经做过批更新的,具体可看这里,遗憾的是如他所说:“放弃了对于复杂表达式树的解析,不支持item.Introduction.Length < 10这种条件”,另有BLACK JACK通过正则式匹配查询语句,可看这里,但是我对那个正则表达式不是很放心,毕竟SQL的变化太多。结合了前面两种做法,我也提出我的思路,我们知道IQueryable的查询语句是可以获得的,那么我们能否在批操作时候直接运用这个SQL呢?答案是肯定的,例如我有这么一个SELECT的SQL(OrderId是主键):
SELECT * from Order WHERE Order.OrderId = 123;
那么可以想办法更新这些查询结果中的记录,SQL语句可以这么写:
UPDATE [Order]
SET [XXX] = xxxx
FROM [dbo].[Order ] AS T1 INNER JOIN (
SELECT *
FROM [Order]
WHERE [Order].[OrderId ] = 123
) AS T2 ON (T1.[OrderId] = T2.[OrderId])
private static string GetJoinCondition<TEntity>(this Table<TEntity> table)
where TEntity : class
{
MetaTable metaTable = table.Context.Mapping.GetTable(typeof(TEntity));
var keys = from member in metaTable.RowType.DataMembers
where member.IsPrimaryKey
select new { member.MappedName };
StringBuilder joinCondition = new StringBuilder();
foreach (var key in keys)
{
joinCondition.AppendFormat("T1.[{0}] = T2.[{0}] AND ", key.MappedName);
}
string join = joinCondition.Remove(0, joinCondition.Length - 5); //"remove and string"
return join;
}
private static string GetTableName<TEntity>(this Table<TEntity> table) where TEntity : class
{
var entityType = typeof(TEntity);
var metaTable = table.Context.Mapping.GetTable(entityType);
string name = metaTable.TableName;
string[] arr = name.Split('.');
foreach (string str in arr)
{
name = name.Replace(str, String.Format("[{0}]", str));
}
return name;
}
private static string GetFormatCmdText<TEntity>(Table<TEntity> table, IQueryable<TEntity> entities ) where TEntity : class
{
string joinCondition = table.GetJoinCondition();
DbCommand selectCommand = table.Context.GetCommand( entities );
string selectSql = selectCommand.CommandText;
string join = String.Format("FROM {0} AS T1 INNER JOIN (\r\n\r\n{1}\r\n\r\n) AS T2 ON ({2})\r\n", table.GetTableName(), selectSql, joinCondition);
return "UPDATE " + table.GetTableName() + " SET {0}" + join;
}
where TEntity : class
{
MetaTable metaTable = table.Context.Mapping.GetTable(typeof(TEntity));
var keys = from member in metaTable.RowType.DataMembers
where member.IsPrimaryKey
select new { member.MappedName };
StringBuilder joinCondition = new StringBuilder();
foreach (var key in keys)
{
joinCondition.AppendFormat("T1.[{0}] = T2.[{0}] AND ", key.MappedName);
}
string join = joinCondition.Remove(0, joinCondition.Length - 5); //"remove and string"
return join;
}
private static string GetTableName<TEntity>(this Table<TEntity> table) where TEntity : class
{
var entityType = typeof(TEntity);
var metaTable = table.Context.Mapping.GetTable(entityType);
string name = metaTable.TableName;
string[] arr = name.Split('.');
foreach (string str in arr)
{
name = name.Replace(str, String.Format("[{0}]", str));
}
return name;
}
private static string GetFormatCmdText<TEntity>(Table<TEntity> table, IQueryable<TEntity> entities ) where TEntity : class
{
string joinCondition = table.GetJoinCondition();
DbCommand selectCommand = table.Context.GetCommand( entities );
string selectSql = selectCommand.CommandText;
string join = String.Format("FROM {0} AS T1 INNER JOIN (\r\n\r\n{1}\r\n\r\n) AS T2 ON ({2})\r\n", table.GetTableName(), selectSql, joinCondition);
return "UPDATE " + table.GetTableName() + " SET {0}" + join;
}
按照上面的举例,直接调用上面的GetFormatCmdText扩展方法我们将得到如下SQL:
UPDATE [Order]
SET {0}
FROM [dbo].[Order ] AS T1 INNER JOIN (
SELECT *
FROM [Order]
WHERE [Order].[OrderId ] = 123
) AS T2 ON (T1.[OrderId] = T2.[OrderId])
http://blogs.msdn.com/mattwar/archive/2007/07/31/linq-building-an-iqueryable-provider-part-ii.aspx
里面有ExpressionVisitor类可供使用,代码我就不贴出来了,需要的朋友可浏览上面的链接。通过代码我们可以获取赋值表达式的MemberInitExpression对象,把它转换成SET的SQL语句片段也很简单:
private static string GetDbSetString<TEntity>( MemberInitExpression memberInitExpression, Table<TEntity> table, DbCommand updateCommand ) where TEntity : class
{
Type entityType = typeof(TEntity);
if (memberInitExpression.Type != entityType)
{
throw new NotImplementedException( string.Format( "the expression should be '{0}'.", entityType ) );
}
StringBuilder setSB = new StringBuilder();
string tableName = table.GetDbName();
MetaTable metaTable = table.Context.Mapping.GetTable( entityType );
var cols = from member in metaTable.RowType.DataMembers
select new { member.MappedName };
foreach ( var binding in memberInitExpression.Bindings )
{
MemberAssignment assignment = binding as MemberAssignment;
if ( binding == null )
{
throw new NotImplementedException( String.Format("the property '{0}' bindings is not a type MemberAssignment.", binding.Member.Name) );
}
ParameterExpression entityParam = null;
ExpressionVisitor<ParameterExpression>.Visit(assignment.Expression, p =>
{
if (p.Type == entityType)
entityParam = p;
return p;
}
);
string name = binding.Member.Name;
var col = ( from c in cols
where c.MappedName == name
select c ).FirstOrDefault();
if ( col == null )
{
throw new ArgumentOutOfRangeException(name, string.Format("not found the field on the {0} table", tableName));
}
if (entityParam == null)
{
object constant = Expression.Lambda(assignment.Expression, null).Compile().DynamicInvoke();
if (constant == null)
{
setSB.AppendFormat("[{0}] = NULL, ", col.MappedName);
}
else
{
setSB.AppendFormat("[{0}] = @p{1}, ", col.MappedName, updateCommand.Parameters.Count);
updateCommand.Parameters.Add(new SqlParameter(string.Format("@p{0}", updateCommand.Parameters.Count), constant));
}
}
else
{
throw NotSupportedException("not support the method call expression");
}
}
string setSqlString = setSB.ToString();
return setSqlString.Substring(0, setStatements.Length - 2); // remove ', '
}
{
Type entityType = typeof(TEntity);
if (memberInitExpression.Type != entityType)
{
throw new NotImplementedException( string.Format( "the expression should be '{0}'.", entityType ) );
}
StringBuilder setSB = new StringBuilder();
string tableName = table.GetDbName();
MetaTable metaTable = table.Context.Mapping.GetTable( entityType );
var cols = from member in metaTable.RowType.DataMembers
select new { member.MappedName };
foreach ( var binding in memberInitExpression.Bindings )
{
MemberAssignment assignment = binding as MemberAssignment;
if ( binding == null )
{
throw new NotImplementedException( String.Format("the property '{0}' bindings is not a type MemberAssignment.", binding.Member.Name) );
}
ParameterExpression entityParam = null;
ExpressionVisitor<ParameterExpression>.Visit(assignment.Expression, p =>
{
if (p.Type == entityType)
entityParam = p;
return p;
}
);
string name = binding.Member.Name;
var col = ( from c in cols
where c.MappedName == name
select c ).FirstOrDefault();
if ( col == null )
{
throw new ArgumentOutOfRangeException(name, string.Format("not found the field on the {0} table", tableName));
}
if (entityParam == null)
{
object constant = Expression.Lambda(assignment.Expression, null).Compile().DynamicInvoke();
if (constant == null)
{
setSB.AppendFormat("[{0}] = NULL, ", col.MappedName);
}
else
{
setSB.AppendFormat("[{0}] = @p{1}, ", col.MappedName, updateCommand.Parameters.Count);
updateCommand.Parameters.Add(new SqlParameter(string.Format("@p{0}", updateCommand.Parameters.Count), constant));
}
}
else
{
throw NotSupportedException("not support the method call expression");
}
}
string setSqlString = setSB.ToString();
return setSqlString.Substring(0, setStatements.Length - 2); // remove ', '
}
需要注意的是,上面的代码参数中的updateCommand,由于我们的updateCommand其实是通过IQueryable获得的,所以里面可能带了参数,因此我们赋值用的参数必须向这个Command中添加,最后就是语句拼接和参数处理了,也就是完成扩展方法:
public static int Update<TEntity>(this Table<TEntity> table, Expression<Func<TEntity, bool>> predicate, Expression<Func<TEntity, TEntity>> evaluator) where TEntity : class
{
IQueryable<TEntity> query = table.Where(predicate);
DbCommand updateCommand = table.Context.GetCommand(query);
string commandText = table.GetFormatCmdText(query);
string setString = null;
int count = 1;
ExpressionVisitor<MemberInitExpression>.Visit(evaluator, expression=>
{
if(count > 1)
{
throw new NotSupportedException("multi MemberInitExpression is not allowed for the evaluator parameter");
}
count ++;
setString = GetDbSetString(expression, table, updateCommand);
return expression;
});
if(String.IsNullOrEmpty(setString))
{
throw new NotImplementedException("Current MemberInitExpression is not include any operation");
}
commandText = String.Format(commandText, setString);
var parameters = from p in updateCommand.Parameters.Cast<DbParameter>()
select p.Value;
return table.Context.ExecuteCommand(commandText, parameters.ToArray());
}
{
IQueryable<TEntity> query = table.Where(predicate);
DbCommand updateCommand = table.Context.GetCommand(query);
string commandText = table.GetFormatCmdText(query);
string setString = null;
int count = 1;
ExpressionVisitor<MemberInitExpression>.Visit(evaluator, expression=>
{
if(count > 1)
{
throw new NotSupportedException("multi MemberInitExpression is not allowed for the evaluator parameter");
}
count ++;
setString = GetDbSetString(expression, table, updateCommand);
return expression;
});
if(String.IsNullOrEmpty(setString))
{
throw new NotImplementedException("Current MemberInitExpression is not include any operation");
}
commandText = String.Format(commandText, setString);
var parameters = from p in updateCommand.Parameters.Cast<DbParameter>()
select p.Value;
return table.Context.ExecuteCommand(commandText, parameters.ToArray());
}
有了这个扩展我们就可以这样更新数据了:
using (DataClasses1DataContext context = new DataClasses1DataContext(ConnectionString))
{
context.Orders.Update(o => o.OrderId == 123, o => new Orders() { Number = "TTT" });
}
{
context.Orders.Update(o => o.OrderId == 123, o => new Orders() { Number = "TTT" });
}
这个扩展并非完美无暇,可能有朋友注意了上面代码中GetDbSetString方法有这么一句:
else
{
throw NotSupportedException("not support the method call expression");
}
经发现有时候Expression<Func<TEntity, TEntity>> evaluator表达式的可能产生内嵌的MethodCallExpression的,对于这样的表达式我没有更好的方式来解析,因此放弃了这种情况的处理,例如:
o=> new Order() {Number= o.Number.Number+ " AAA"}
对于这种内联调用这个扩展暂时无能为力,好了,文章完,有了UPDATE的思路DELETE也就可以同样的方式扩展了。