使用Entity Framework克隆Entity对象及其所有关联的子对象






4.75/5 (7投票s)
使用 Entity Framework、LINQ 和反射克隆实体对象及其所有子项。
引言
在现实世界中,有很多场景需要通过简单的接口复制大量记录及其所有相关记录,这些接口稍作修改即可重用,而无需从头开始创建。在本文中,我们将看到一种在 LINQ to Entities 中解决此问题的方法。本文试图解决以下问题:
- 实体延迟加载:一次方法调用即可加载实体及其所有关联。
- 实体克隆:如何在 LINQ to Entities (Entity Framework) 中克隆实体及其所有关联。这包括在克隆后将数据重新写入数据库。
注意:新版本修复了错误“已存在具有相同键的对象”
背景
本文使用了以下技术:
- 序列化 - 用于克隆实体。
- 反射 - 用于动态调用方法和访问属性。反射用于实现加载子对象和清除克隆对象中的实体引用。
- LINQ - LINQ 用于整个实现过程中。
- 扩展方法 - 实体对象被扩展以实现所描述的功能。
本文将更侧重于解决方案和逻辑,而不是解释这些技术的基础知识。
使用代码
示例项目是一个简单的员工主数据创建应用程序,包含一些 CRUD 操作。该项目使用 ASP.NET MVC 和 Entity Framework 实现。
附加项目中的文件夹和文件详细信息
- DbScript/efcloningsample.sql - 创建数据库对象的数据库脚本。
- CLonehelper.cs - 此类包含加载实体、克隆实体和清除实体引用的扩展方法。
- View/ Employeemaster - 所有视图都包含数据库中定义的模型的视图内容。
- HomeController - 用于员工 CRUD 操作的控制器。
- Models/ EmpModel.edmx - 从数据库创建的 EF 模型。
CloneHelper.CS 的实现细节
CloneHelper
类包含所有逻辑和实现。现在我们将查看其方法及其在实现中的作用。
克隆
这是一个扩展到 EntityObject
的扩展方法。它使用 DataContractSerializer
来序列化对象。克隆的实体将返回给调用者。
public static T Clone<t>(this T source) where T:EntityObject {
var obj = new System.Runtime.Serialization.DataContractSerializer(typeof(T));
using (var stream = new System.IO.MemoryStream())
{
obj.WriteObject(stream, source);
stream.Seek(0, System.IO.SeekOrigin.Begin);
return (T)obj.ReadObject(stream);
}
}
LoadAllChild
LoadAllChild
将从数据库加载基本实体所有关联的子对象。LINQ 查询将获取子对象,并使用反射,在所有子对象上调用 Load
方法。对对象进行检查以确保它们不会被加载两次。这是通过惰性加载逻辑实现的。
public static EntityObject LoadAllChild(this EntityObject source)
{
List<propertyinfo> PropList = (from a in source.GetType().GetProperties()
where a.PropertyType.Name == "EntityCollection`1"
select a).ToList();
foreach (PropertyInfo prop in PropList)
{
object instance = prop.GetValue(source, null);
bool isLoad =
(bool)instance.GetType().GetProperty("IsLoaded").GetValue(instance, null);
if (!isLoad)
{
MethodInfo mi = (from a in instance.GetType().GetMethods()
where a.Name == "Load" && a.GetParameters().Length == 0
select a).FirstOrDefault();
mi.Invoke(instance, null);
}
}
return (EntityObject)source;
}
ClearEntityReference
此方法用于清除克隆实体上的实体引用。克隆的实体将在清除实体引用后附加到对象。克隆的对象应被视为新数据,并应创建新的主键并与参照完整性关联。一旦在克隆对象上清除了实体引用,框架将为关联创建临时键(将其视为新实体并遵循相同逻辑)。当数据移至数据库时,将创建原始键并将其关联回 EF 模型。
此方法也是 EntityObject
的扩展。bcheckHierarchy
属性用于决定仅应用于基本对象的逻辑,还是需要应用于所有子对象。基本上有两个属性:EntityKey
和 EntityReferences
。在此过程中,EntityKey
设置为 null
。还会递归调用以清除子对象中的实体。
public static EntityObject ClearEntityReference(this EntityObject source,
bool bcheckHierarchy)
{
return source.ClearEntityObject(bcheckHierarchy);
}
private static T ClearEntityObject<t>(this T source,
bool bcheckHierarchy) where T : class
{
//Throw if passed object has nothing
if (source == null) { throw new Exception("Null Object cannot be cloned"); }
// get the TYpe of passed object
Type tObj = source.GetType();
// check object Passed does not have entity key Attribute
if (tObj.GetProperty("EntityKey") != null)
{
tObj.GetProperty("EntityKey").SetValue(source, null, null);
}
//bcheckHierarchy this flag is used to check
//and clear child object releation keys
if (!bcheckHierarchy)
{
return (T)source;
}
// Clearing the Entity for Child Objects
// Using the Linq get only Child Reference objects from source object
List<propertyinfo> PropList = (from a in source.GetType().GetProperties()
where a.PropertyType.Name.ToUpper() == "ENTITYCOLLECTION`1"
select a).ToList();
// Loop thorough List of Child Object and Clear the Entity Reference
foreach (PropertyInfo prop in PropList)
{
IEnumerable keys =
(IEnumerable)tObj.GetProperty(prop.Name).GetValue(source, null);
foreach (object key in keys)
{
//Clearing Entity Reference from Parnet Object
var ochildprop = (from a in key.GetType().GetProperties()
where a.PropertyType.Name == "EntityReference`1"
select a).SingleOrDefault();
ochildprop.GetValue(key, null).ClearEntityObject(false);
//Clearing the the Entity Reference from
//Child object .This will recrusive action
key.ClearEntityObject(true);
}
}
return (T)source;
}
示例实现
附加的示例应用程序有一个员工主表,带有两个子表和一个孙子表。
- Employee (Level 1)
- EmpAddress (Level 2)
- EmpBasesal (Level 2)
- EmpSalDetail (Level 3)
当用户想要克隆员工数据时,他会选择一个员工并克隆它。用户有两个选项:仅克隆父/基本实体,或克隆父、子和孙子。
实现没有对关联级别的深度/广度做任何限制。它将支持任何级别。
示例具有三级层级
public ActionResult Clone(int id)
{
//Load the Empbasic into Context
var Emp = (from a in osamp.Employees
where a.EMPID == id
select a).FirstOrDefault();
//Load EmpAddress using lazy load
// Emp.EmpAddresses.Load();
//Call the extesnion Method Clone(cast is requried
//as it was a Entity Object)
var EMpnew = Emp.Clone();
//Clear Entity Values of New Object
EMpnew.ClearEntityReference(false );
//detach the Load empbasic from Context
osamp.Detach(Emp);
//Add new Clone Object and save it DB
osamp.AddToEmployees((Employee)EMpnew );
osamp.SaveChanges();
osamp.AcceptAllChanges();
return RedirectToAction("index"); ;
}
public ActionResult CloneChild(int id)
{
var Emp = (from a in osamp.Employees
where a.EMPID == id
select a).FirstOrDefault();
//Load EmpAddress using lazy load
Emp.LoadAllChild();
//Call the extesnion Method Clone(cast is requried
//as it was a Entity Object)
var Empnew =Emp.Clone();
//Detach the real Object
osamp.Detach(Emp);
//Clear the Entities in the Cloned object
Empnew.ClearEntityReference(true);
//Attach and save the clone in database
osamp.AddToEmployees((Employee)Empnew);
osamp.SaveChanges();
return RedirectToAction("index");
}
系统要求
代码和示例将在 .NET Framework 4.0 上运行。
参考文献
以下博客文章在很大程度上提供了帮助:
- http://damieng.com/blog/2009/04/12/linq-to-sql-tips-and-tricks-2.
- http://naspinski.net/post/Cloning-an-Entity-in-Linq-to-Entities.aspx
结论
我写了一篇 博客,讨论了使用 ADO.NET 3.5 的相同实现,但仅限于两级关联,并且一些方法已过时。此代码将在 .NET 3.5 上存在一些问题。只需将 CloneHelper
类包含到项目中即可实现此解决方案。希望这对一些实际场景有所帮助,并提高开发人员的生产力。祝您编码愉快!