Entity Framework - 只更新非空值

7

这对我来说有点新鲜。我被要求编写一个ETL程序,将两个数据集加载到同一张表中。数据集#1是完整的,并包含表中的所有数据。然而,数据集#2只包含需要覆盖在第一个数据集上的更改。请看下面:

// 数据集#1:小部件表

+----+------+------+------+------+
| ID | COL1 | COL2 | COL3 | COL4 |
+----+------+------+------+------+
| 1  | abcd | abcd | abcd | abcd |
+----+------+------+------+------+
| 2  | abcd | abcd | abcd | abcd |
+----+------+------+------+------+

// 数据集 #2: Widgets_Changes 表格

+----+------+------+------+------+
| ID | COL1 | COL2 | COL3 | COL4 |
+----+------+------+------+------+
| 1  |      | efgh |      | ijkl |
+----+------+------+------+------+
| 2  | mnop |      | qrst |      |
+----+------+------+------+------+

// 预期结果:小部件具有所有更改

+----+------+------+------+------+
| ID | COL1 | COL2 | COL3 | COL4 |
+----+------+------+------+------+
| 1  | abcd | efgj | abcd | ijkl |
+----+------+------+------+------+
| 2  | mnop | abcd | qrst | abcd |
+----+------+------+------+------+

显而易见的方法(我试图避免)是将第一个表格中的每个小部件分离出来,并逐个属性进行比较:

// Simplified example:
using ( var db = new MyEntityDatabase() ){

   var widget      = from p in db.Widgets select p where p.ID == 1;
   var widget_diff = from p in db.Widgets_Changes select p where p.ID == 1

   widget.COL1 = widget_diff.COL1 ?? widget.COL1;
   widget.COL2 = widget_diff.COL2 ?? widget.COL2;
   widget.COL3 = widget_diff.COL3 ?? widget.COL3;
   // ...etc

   db.saveChanges();
}

然而,这个特定数据集中有超过200个字段,还有更多遵循同样方法的文件正在到来,但完全有不同的架构。显然,我希望有一种便携式的方法,可以直接运行文件,而不是针对每个数据集硬编码属性比较。
有没有一种方法可以迭代两个对象的属性,并更新不为空的值?

你可以使用反射来实现这个功能,使用 PropertyInfo 来获取属性。 - AD.Net
3个回答

11

首先,您需要使用类似下面这样的代码来选择要更新的实体:

var widget      = db.Widgets.First(p => p.ID == 1);
var widget_diff = db.Widgets_Changes.First(p => p.ID == 1);

现在,你可以简单地使用反射来更新所有字段:
foreach(var toProp in typepf(Widget).GetProperties())
{
    var fromProp= typeof(Widget_Change).GetProperty(toProp.Name);
    var toValue = fromProp.GetValue(widget_diff, null);
    if (toValue != null)
    {
        toProp.SetValue(widget, toValue, null);
    }
}

通过预先构建属性列表,可以加快速度,这样您只需要使用反射一次:

public static class WidgetUtil
{
    public static readonly IEnumerable<Tuple<PropertyInfo, PropertyInfo>> PropertyMap;

    static Util()
    {
        var b = BindingFlags.Public | BindingFlags.Instance;
        PropertyMap = 
            (from f in typeof(Widget).GetProperties(b)
             join t in typeof(WidgetChange).GetProperties(b) on f.Name equals t.Name
             select Tuple.Create(f, t))
            .ToArray();
    }
}

...

foreach(var propertyPair in WidgetUtil.PropertyMap)
{
    var toValue = propertyPair.Item2.GetValue(widget_diff, null);
    if (toValue != null)
    {
        propertyPair.Item1.SetValue(widget, toValue, null);
    }
}

如果你有许多这样的实体类型,甚至可以考虑将其转化为通用的工具:

public static class WidgetUtil<T1, T2>
{
    public static readonly IEnumerable<Tuple<PropertyInfo, PropertyInfo>> PropertyMap;

    static WidgetUtil()
    {
        var b = BindingFlags.Public | BindingFlags.Instance;
        PropertyMap = 
            (from f in typeof(T1).GetProperties(b)
             join t in typeof(T2).GetProperties(b) on f.Name equals t.Name
             select Tuple.Create(f, t))
            .ToArray();
    }
}

我的天啊。这比我能想象的任何东西都要好得多。谢谢! - Daniel Szabo

5
您可能需要使用反射来实现此功能。循环遍历每个小部件/差异的所有属性/字段,获取该属性/字段的值,如果差异为null,则使用原始值。
using(var db = new MyEntityDatabase())
{
    var widget      = from p in db.Widgets select p where p.ID == 1;
    var widget_diff = from p in db.Widgets_Changes select p where p.ID == 1;

    var properties = typeof(MyWidgetType).GetProperties(BindingFlags.Public | BindingFlags.Instance);
    foreach(var property in properties)
    {
         //widget.column = widget_diff.column ?? widget.colum;
         property.SetValue(property.GetValue(widget_diff) ?? property.GetValue(widget), widget);
    }

    //You can do the same for fields here if the entity has any fields (probably not).
}

请注意,如果 widgetwidget_diff 是不同类型的,则此方法将无法正常工作。您需要为每个对象使用适当的 PropertyInfo - p.s.w.g

3

@p.s.w.g的回答非常好,但是当我试图实现它时,遇到了几个错误(例如,您不能使用obj.Equals(null)检查null,null没有Equals方法)。

这里是@p.s.w.g优秀回答的“完整可复制解决方案”(作为副产品)

一个静态的泛型方法InjectNonNull获取你想要更新的源实体和带有null的目标“稀疏”实体,并仅传输目标实体上的非空属性。

    private static class PropertyLister<T1, T2>
    {
        public static readonly IEnumerable<Tuple<PropertyInfo, PropertyInfo>> PropertyMap;

        static PropertyLister()
        {
            var b = BindingFlags.Public | BindingFlags.Instance;
            PropertyMap =
                (from f in typeof(T1).GetProperties(b)
                 join t in typeof(T2).GetProperties(b) on f.Name equals t.Name
                 select Tuple.Create(f, t))
                    .ToArray();
        }
    }


    public static T InjectNonNull<T>(T dest, T src)
    {
        foreach (var propertyPair in PropertyLister<T, T>.PropertyMap)
        {
            var fromValue = propertyPair.Item2.GetValue(src, null);
            if (fromValue != null && propertyPair.Item1.CanWrite)
            {

                propertyPair.Item1.SetValue(dest, fromValue, null);
            }
        }

        return dest;
    }

是的,我之前确实看到过这个问题。toValue.Equals(null) 应该改为 toValue != null 或者 Object.Equals(toValue, null)。我已经更正了我的答案以完整性为重,但加上 propertyPair.Item1.CanWrite 也是一个好主意。 - p.s.w.g

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接