EF:包含所有子对象(包括子子对象)的复制对象(深层复制)

本文关键字:对象 复制 包含所 EF 包括 | 更新日期: 2023-09-27 17:55:51

我有一个表,其属性如下:

Id Name ParentId

ParentId 是主列Id的外键。现在假设我有几行,例如:(仅显示行中的ParentId

    NULL
   /    '
  1      2
        / '
       3   4

现在,假设我们要复制ParentId为 NULL 的行对象及其所有子对象。

  var row = db.FirstOrDefault(x=> x.Id == 1);
  var new_row = new Table1();
  var subrows = row.Table1.ToArray();
        foreach(var row in subrows)
        {
            db.Entry(row).State = System.Data.Entity.EntityState.Detached;
        }
        new_row.Table1 = subrows;
db.Table.Add(new_row);
db.saveChanges();

结果:新插入的结构,如下所示:

    NULL
   /    '
  1      2

我假设只复制了一个子级别。如何复制/插入所有子级别?

编辑:由于分离有助于创建一个副本直到一个级别,这就是我尝试过的:

private void RecursiveDetach(Table1 parent)
        {
            var subrows = parent.Table1.ToArray();
            foreach (var row in subrows)
            {
                if(row.Table1.Count() > 0)
                {
                    RecursiveDetach(row);
                }
                db.Entry(row).State = System.Data.Entity.EntityState.Detached;
            }
        }

但是,现在我收到一个错误:

集合已修改;枚举操作可能无法执行。

EF:包含所有子对象(包括子子对象)的复制对象(深层复制)

我以前不得不这样做。我纯粹在代码中完成了它,递归复制对象并在需要时清理唯一 ID,但我构建的最干净的方法是将对象序列化为 XML,然后反序列化为新对象。该方法效率较低,但非常灵活且易于实施。

//Save object to XML file. Returns filename.
public string SaveObjectAsXML(int id)
{
    //however you get your EF context and disable proxy creation
    var db = GetContext(); 
    bool currentProxySetting = db.Configuration.ProxyCreationEnabled;
    db.Configuration.ProxyCreationEnabled = false;
    //get the data
    var item = db.GetItem(id); //retrieval be unique to your setup, but I have
                               //a more generic solution if you need it. Make
                               //sure you have all the sub items included
                               //in your object or they won't be saved.
    db.Configuration.ProxyCreationEnabled = currentProxySetting;
    //if no item is found, do whatever needs to be done
    if (item == null)
    {                
        return string.Empty;
    }            
    //I actually write my data to a file so I can save states if needed, but you could
    //modify the method to just spit out the XML instead
    Directory.CreateDirectory(DATA_PATH); //make sure path exists to prevent write errors
    string path = $"{DATA_PATH}{id}{DATA_EXT}";
    var bf = new BinaryFormatter();
    using (FileStream fs = new FileStream(path, FileMode.Create))
    {
        bf.Serialize(fs, repair);
    }
    return path;
}
//Load object from XML file. Returns ID.
public int LoadXMLData(string path)
{   
    //make sure the file exists
    if (!File.Exists(path))
    {
        throw new Exception("File not found.");
    }
    //load data from file
    try 
    { 
        using (FileStream fs = new FileStream(path, FileMode.Open)) 
        {
            var item = (YourItemType)new BinaryFormatter().Deserialize(fs);
            db.YourItemTypes.Add(item);
            db.SaveChanges();
            return item.Id;
        }
    }
    catch (Exception ex) {
        //Exceptions here are common when copying between databases where differences in config entries result in mis-matches
        throw;
    }
}

使用很简单。

//save object
var savedObjectFilename = SaveObjectAsXML(myObjID);
//loading the item will create a copy
var newID = LoadXMLData(savedObjectFilename);

祝你好运!

这是第二个完全不同的答案:递归分离整个对象,而不仅仅是父对象。以下内容是作为上下文对象的扩展方法编写的:

    /// <summary>
    /// Recursively detaches item and sub-items from EF. Assumes that all sub-objects are properties (not fields).
    /// </summary>
    /// <param name="item">The item to detach</param>
    /// <param name="recursionDepth">Number of levels to go before stopping. object.Property is 1, object.Property.SubProperty is 2, and so on.</param>
    public static void DetachAll(this DbContext db, object item, int recursionDepth = 3)
    {
        //Exit if no remaining recursion depth
        if (recursionDepth <= 0) return;
        //detach this object
        db.Entry(item).State = EntityState.Detached;
        //get reflection data for all the properties we mean to detach
        Type t = item.GetType();
        var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                          .Where(p => p.GetSetMethod()?.IsPublic == true)  //get only properties we can set                              
                          .Where(p => p.PropertyType.IsClass)              //only classes can be EF objects
                          .Where(p => p.PropertyType != typeof(string))    //oh, strings. What a pain.
                          .Where(p => p.GetValue(item) != null);           //only get set properties
        //if we're recursing, we'll check here to make sure we should keep going
        if (properties.Count() == 0) return;
        foreach (var p in properties)
        {
            //handle generics
            if (p.PropertyType.IsGenericType)
            {
                //assume its Enumerable. More logic can be built here if that's not true.
                IEnumerable collection = (IEnumerable)p.GetValue(item);
                foreach (var obj in collection)
                {
                    db.Entry(obj).State = EntityState.Detached;
                    DetachAll(db, obj, recursionDepth - 1);
                }
            }
            else
            {
                var obj = p.GetValue(item);
                db.Entry(obj).State = EntityState.Detached;
                DetachAll(db, obj, recursionDepth - 1);
            }
        }
    }

需要注意的最重要的事情是配置类型属性 - 表示与对象没有直接关系的数据的对象。这些可能会产生冲突,因此最好确保您的对象不包含它们。

注意:

此方法要求提前填充要复制的所有子对象,以避免延迟加载。 为了确保这一点,我对 EF 查询使用以下扩展:

//Given a custom context object such that CustomContext inherits from DbContext AND contains an arbitrary number of DbSet collections
//which represent the data in the database (i.e. DbSet<MyObject>), this method fetches a queryable collection of object type T which
//will preload sub-objects specified by the array of expressions (includeExpressions) in the form o => o.SubObject.
public static IQueryable<T> GetQueryable<T>(this CustomContext context, params Expression<Func<T, object>>[] includeExpressions) where T : class
{
    //look through the context for a dbset of the specified type
    var property = typeof(CustomContext).GetProperties().Where(p => p.PropertyType.IsGenericType &&
                                                                    p.PropertyType.GetGenericArguments()[0] == typeof(T)).FirstOrDefault();
    //if the property wasn't found, we don't have the queryable object. Throw exception
    if (property == null) throw new Exception("No queryable context object found for Type " + typeof(T).Name);
    //create a result of that type, then assign it to the dataset
    IQueryable<T> source = (IQueryable<T>)property.GetValue(context);
    //return 
    return includeExpressions.Aggregate(source, (current, expression) => current.Include(expression));
}

此方法假定您有一个自定义上下文对象,该对象继承自DbContext并包含对象的DbSet<>集合。它将找到合适的DbSet<T>并返回一个可查询的集合,该集合将在对象中预加载指定的子类。这些被指定为表达式数组。例如:

//example for object type 'Order'
var includes = new Expression<Func<Order, object>>[] {
    o => o.SalesItems.Select(p => p.Discounts), //load the 'SalesItems' collection AND the `Discounts` collection for each SalesItem
    o => o.Config.PriceList,                    //load the Config object AND the PriceList sub-object
    o => o.Tenders,                             //load the 'Tenders' collection
    o => o.Customer                             //load the 'Customer' object
};

为了检索我的可查询集合,我现在这样调用它:

var queryableOrders = context.GetQueryable(includes);

同样,这里的目的是创建一个可查询的对象,该对象将仅热切地加载您实际想要的子对象(和子子对象)。

若要获取特定项,请像使用任何其他可查询源一样使用它:

var order = context.GetQueryable(includes).FirstOrDefault(o => o.OrderNumber == myOrderNumber);

请注意,您也可以提供内联的包含表达式;但是,您需要指定泛型:

//you can provide includes inline if you just have a couple
var order = context.GetQueryable<Order>(o => o.Tenders, o => o.SalesItems).FirstOrDefault(o => o.OrderNumber == myOrderNumber);