最近搞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

                          FROM [dbo].[Order ] AS T1 INNER JOIN (

                          SELECT *
                          FROM [Order]
                          WHERE [Order].[OrderId ] = 123
                         ) AS T2 ON (T1.[OrderId] = T2.[OrderId])

     很显然,子查询里的语句是直接获取的,这样我们的任务就变成构造SET [XXX] = xxxx....的字符串和join语句了,SET后面会说,这里直接给JOIN语句的构造代码:
     
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;
        }


       按照上面的举例,直接调用上面的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])

     现在的问题就是有SET后的赋值SQL串了,这也是批更新的难点,表达式树的解析很复杂,所幸的是有这么一个老外的博客提供了现成的代码给我们用:

     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 ', '
        }

    需要注意的是,上面的代码参数中的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());
        }


     有了这个扩展我们就可以这样更新数据了:
     
using (DataClasses1DataContext context = new DataClasses1DataContext(ConnectionString))
            
{
                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也就可以同样的方式扩展了。
posted on 2008-05-09 18:25  sharping  阅读(2191)  评论(16编辑  收藏  举报