Last week I wrote about fast batch deletions. In this post I’ll show how to do the same for modifications.
Let’s assume we want to replace the ‘State’ property with ‘CA’ and CostCenter with 123 for all records where the ‘City’ is ‘San Francisco’. The recommended DevExpress approach would be something like the following:
The problem with the above code is that every record must be loaded and then an individual UPDATE command is generated for each modification. This is necessary for the business logic to be applied correctly (such as the code in methods such as OnSaving()). It is also necessary to handle record locking.
If you know that your objects do not require any of this processing, you can use use direct SQL as described in the XPO documentation. This however requires knowledge of the underlying database table and is not very versatile, (although the DevExpress.Data.Filtering.CriteriaToWhereClauseHelper() can help if you choose this route).
However, there is a method similar to the one described in the previous post which is equivalent to the direct SQL approach, but is much easier to use. The approach makes use of an extension method on the Session class.
Example
Since the extension method is somewhat more complicated than for the Delete case, I will start by showing an example of use before drilling into the supporting code.
The Update method takes an Expression<Func<T>> as the first parameter which allows us to pass in an anonymous type which serves as a template for the modification. This way we get strong typing for the property values.
publicclassPropertyValueStore:List<KeyValuePair<XPMemberInfo,Object>>{}publicstaticclassSessionExtensions{publicstaticPropertyValueStoreCreatePropertyValueStore(XPClassInfoclassInfo,MemberInitExpressionmemberInitExpression){PropertyValueStorepropertyValueStore=newPropertyValueStore();/// Parse each expression binding within the anonymous class. /// Each binding represents a property assignment within the IXPObject./// Add a KeyValuePair for the corresponding MemberInfo and (invoked) value.foreach(varbindinginmemberInitExpression.Bindings){varassignment=bindingasMemberAssignment;if(binding==null){thrownewNotImplementedException("All bindings inside the MemberInitExpression are expected to be of type MemberAssignment.");}// Get the memberInfo corresponding to the property name.stringmemberName=binding.Member.Name;XPMemberInfomemberInfo=classInfo.GetMember(memberName);if(memberInfo==null)thrownewArgumentOutOfRangeException(memberName,String.Format("The member {0} of the {1} class could not be found.",memberName,classInfo.FullName));if(!memberInfo.IsPersistent)thrownewArgumentException(memberName,String.Format("The member {0} of the {1} class is not persistent.",memberName,classInfo.FullName));// Compile and invoke the assignment expression to obtain the contant value to add as a parameter.varconstant=Expression.Lambda(assignment.Expression,null).Compile().DynamicInvoke();// Add the propertyValueStore.Add(newKeyValuePair<XPMemberInfo,Object>(memberInfo,constant));}returnpropertyValueStore;}publicstaticModificationResultUpdate<T>(thisSessionsession,Expression<Func<T>>evaluator,CriteriaOperatorcriteria)whereT:IXPObject{if(ReferenceEquals(criteria,null))criteria=CriteriaOperator.Parse("True");XPClassInfoclassInfo=session.GetClassInfo(typeof(T));varbatchWideData=newBatchWideDataHolder4Modification(session);intrecordsAffected=(int)session.Evaluate<T>(CriteriaOperator.Parse("Count()"),criteria);/// Parse the Expression./// Expect to find a single MemberInitExpression.PropertyValueStorepropertyValueStore=null;intmemberInitCount=1;evaluator.Visit<MemberInitExpression>(expression=>{if(memberInitCount>1){thrownewNotImplementedException("Only a single MemberInitExpression is allowed for the evaluator parameter.");}memberInitCount++;propertyValueStore=CreatePropertyValueStore(classInfo,expression);returnexpression;});MemberInfoCollectionproperties=newMemberInfoCollection(classInfo,propertyValueStore.Select(x=>x.Key).ToArray());List<ModificationStatement>collection=UpdateQueryGenerator.GenerateUpdate(classInfo,properties,criteria,batchWideData);foreach(UpdateStatementupdateStatementincollection.OfType<UpdateStatement>()){for(inti=0;i<updateStatement.Parameters.Count;i++){Objectvalue=propertyValueStore[i].Value;if(valueisIXPObject)updateStatement.Parameters[i].Value=((IXPObject)(value)).ClassInfo.GetId(value);elseupdateStatement.Parameters[i].Value=value;}updateStatement.RecordsAffected=recordsAffected;}returnsession.DataLayer.ModifyData(collection.ToArray<ModificationStatement>());}}
Limitations
There is currently no way to refer to another field within the assignment expressions - you can only set the value to an OperandValue. So you cannot do
123456789
uow.Update<MyObject>(o=>newMyObject(uow){// Does not Compile !!!Property1=o.Property2,// Neither does this !!!Property3=o.Property3+1},null);
In order to fix this, the evaluator has to be of type Expression<Func<T, T>> instead of Expression<Func<T>>, and then you can use expression trees to get an assignment expression. But then there is no way to pass it to a DevExpress UpdateStatement.Parameter as an OperandValue.