2016-07-07 76 views
1

我必须具有类似性质的表:EF:复制对象的所有孩子,包括子子儿(深层副本)

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; 
      } 
     } 

不过,现在我得到一个错误:

Collection was modified; enumeration operation may not execute.

+0

您是使用自引用关系还是涉及两个不同的实体? – octavioccl

+0

我没有得到你想要的,但在这里更多的上下文'实体db =新实体();'这里是如何定义外键。 ALTER TABLE [dbo]。[TABLE] WITH NOCHECK ADD CONSTRAINT [FK_Table_Table] FOREIGN KEY([ParentId]) REFERENCES [dbo]。[Table]([Id])' –

回答

0

我以前必须这样做。我完全用代码完成了它,在需要的地方递归地复制对象并清理唯一的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); 

祝你好运!

0

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

/// <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); 
+0

如果递归深度未知,该怎么办? –

+0

我也用我试过的东西修改了这个问题。建议是否有简单的方法来解决'收集修改'错误。 –

+0

只要您没有任何循环引用(即'Person.Address.State.Person'),就不需要递归检查。在这种情况下,您可以删除递归深度,并且您也不会收集修改后的错误。 – Daniel