LINQ to SQL 的通用基类





3.00/5 (8投票s)
一个用于 LINQ to SQL 的通用基类,您可以利用它轻松地实现访问数据库的代码。
引言
语言集成查询 (LINQ) 是 Visual Studio 2008 中的一组功能,它将强大的查询功能扩展到 C# 和 Visual Basic 的语言语法中。作为 LINQ 的一部分,LINQ to SQL 提供了一个运行时体系结构,用于将关系数据管理为对象。在某种程度上,它等同于基于 .NET 框架的 ORM 工具或框架,如 NHibernate 和 Castle。当我们想访问数据库时,它逐渐成为我们的首选。
在 LINQ to SQL 中,关系数据库的数据模型中的所有变量都可以是强类型的,这提供了编译时验证和 IntelliSense 的优势。我们可以使用查询表达式(包括查询语法和方法语法)从数据库中获取数据。
然而,强类型特性不利于抽象数据操作的通用逻辑,因此开发人员必须定义一个特定的类来处理实体对象。这会导致大量重复代码。如果我们能够实现一个封装了 Select、Where、Add、Update 和 Delete 等通用操作的基类,那么它将对 N 层应用程序非常有用。
使用代码
使用我的 LINQ to SQL 基类,您可以直接实现该类来访问数据库,而无需编写一行代码。您应该做的是让您的类继承我的基类,如下所示:
public class EmployeeAccessor:AccessorBase<Employee,NorthwindDataContext>
{
}
现在,您可以通过它添加、更新、删除或选择数据对象。请参考单元测试方法。
[TestMethod()]
public void UpdateEmployee()
{
EmployeeAccessor accessor = new EmployeeAccessor();
IList<Employee> entities = accessor.Where(e => e.EmployeeID == 1);
if (entities != null && entities.Count > 0)
{
entities[0].FirstName = "Bruce";
entities[0].LastName = "Zhang";
accessor.Update(entities[0],true,true);
}
}
您甚至可以让 Employee
实体直接继承我的基类。
public partial class Employee : AccessorBase<Employee, NorthwindDataContext>
{
}
它的行为与马丁·福勒(Martin Fowler)在其题为《贫血领域模型》的文章中所说的富领域模型非常相似。
基类的实现。
查询功能的实现非常简单。我们可以调用 LINQ 的 DataContext 中的一个名为 GetTable<TEntity>()
的方法,然后调用 GetTable<TEntity>()
方法的某些 LINQ 操作,并将 Lambda 表达式传递给它。
public IList<TEntity> Where(Func<TEntity, bool> predicate)
{
InitDataContext();
return m_context.GetTable<TEntity>().Where(predicate).ToList<TEntity>();
}
我们也可以公开一个接受条件子句的方法,使用动态查询。
public IList<TEntity> Where(string predicate, params object[] values)
{
InitDataContext();
return m_context.GetTable<TEntity>().Where(predicate, values).
ToList<TEntity>();
}
Update
方法(也包括 Delete
方法)的实现更为复杂。虽然我们可以使用 LINQ 引入的 Attach
方法,但它们有一些限制。因此,我提供了几个用于不同情况的 Update
方法。
首先,我们必须考虑实体是否与其他实体有关联。如果有关联,我们必须从它那里移除关联。我使用反射技术定义了一个 Detach
方法,如下所示:
private void Detach(TEntity entity)
{
foreach (FieldInfo fi in entity.GetType().
GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (fi.FieldType.ToString().Contains("EntityRef"))
{
var value = fi.GetValue(entity);
if (value != null)
{
fi.SetValue(entity, null);
}
}
if (fi.FieldType.ToString().Contains("EntitySet"))
{
var value = fi.GetValue(entity);
if (value != null)
{
MethodInfo mi = value.GetType().GetMethod("Clear");
if (mi != null)
{
mi.Invoke(value, null);
}
fi.SetValue(entity, value);
}
}
}
}
对于 EntityRef<T>
字段,我们可以通过调用 FieldInfo
的 SetValue
将它们的值设置为 null
来移除关联。但是,我们不能以同样的方式处理 EntitySet
,因为它是一个集合。如果设置为 null
,它将抛出异常。因此,我获取该字段的方法信息并调用 Clear
方法来清空该集合中的所有项。
对于更新操作,我们可以传递已更改的实体并更新它。代码片段如下所示:
/// <summary>
/// Update the entity according to the passed entity.
/// If isModified is true, the entity must have timestamp properties
/// (means isVersion attribute on the Mapping is true).
/// If false, the entity's properties must set the UpdateCheck attribute
/// to UpdateCheck.Never on the Mapping (There are some mistakes still)
/// </summary>
/// <param name="changedEntity">It shoulde be changed
/// in another datacontext</param>
/// <param name="isModified">It indicates the entity should be considered dirty
/// and forces the context to add the entity to
/// the list of changed objects.</param>
/// <param name="hasRelationship">Has Relationship between the entitis</param>
public void Update(TEntity changedEntity, bool isModified, bool hasRelationship)
{
InitDataContext();
try
{
if (hasRelationship)
{
//Remove the relationship between the entities
Detach(changedEntity);
}
m_context.GetTable<TEntity>().Attach(changedEntity, isModified);
SubmitChanges(m_context);
}
catch (InvalidCastException ex)
{
throw ex;
}
catch (NotSupportedException ex)
{
throw ex;
}
catch (Exception ex)
{
throw ex;
}
}
public void UpdateWithTimeStamp(TEntity changedEntity)
{
Update(changedEntity, true);
}
public void UpdateWithNoCheck(TEntity changedEntity)
{
Update(changedEntity, false);
}
请注意,将被更新的实体必须有一个时间戳,否则将抛出异常。
关于移除实体之间关联的正确性,请不要担心。Attach
方法仅负责将实体关联到一个新的 DataContext
实例以跟踪更改。当您提交更改时,DataContext 将检查映射数据库中的实际值,并根据传递的实体来更新或删除记录。特别是,如果您想在数据库中实现级联删除,您应该在数据库中采取级联操作,例如在主键表和外键表之间。
如果实体与其他实体没有关联,您可以将 "false
" 传递给 hasrelationship 参数,如下所示:
accessor.Update(entities[0],true,false);
为已存在的数据表创建时间戳列是很糟糕的,它可能会影响您的整个系统。(我强烈建议您为数据库创建时间戳列,这会提高性能,因为它在处理并发时不会检查所有列是否已更改。)我解决此问题的方法是传递原始实体,并使用 Action<TEntity>
委托来更新它,如下所示:
/// <summary>
/// Update the entity which was passed
/// The changedEntity cann't have the relationship between the entities
/// </summary>
/// <param name="originalEntity">It must be unchanged entity
/// in another data context</param>
/// <param name="update">It is Action<T>delegate,
/// it can accept Lambda Expression.</param>
/// <param name="hasRelationship">Has relationship between the entities</param>
public void Update(TEntity originalEntity,
Action<TEntity> update, bool hasRelationship)
{
InitDataContext();
try
{
if (hasRelationship)
{
//Remove the relationship between the entitis
Detach(originalEntity);
}
m_context.GetTable<TEntity>().Attach(originalEntity);
update(originalEntity);
SubmitChanges(m_context);
}
catch (InvalidCastException ex)
{
throw ex;
}
catch (NotSupportedException ex)
{
throw ex;
}
catch (Exception ex)
{
throw ex;
}
}
并发问题。
考虑到并发问题,我通过定义一个名为 SubmitChanges
的虚拟方法提供了默认实现。它将遵循 **最后提交获胜** 的规则来处理并发冲突。该方法如下所示:
protected virtual void SubmitChanges(TContext context)
{
try
{
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException)
{
context.ChangeConflicts.ResolveAll(RefreshMode.KeepCurrentValues);
context.SubmitChanges();
}
catch (Exception ex)
{
throw ex;
}
}
如果您想更改处理并发冲突的策略,可以在子类中重写该方法。
其他
您可能已经注意到,InitDataContext
方法在所有访问数据的方法中都被调用。其实现如下:
private TContext m_context = null;
private TContext CreateContext()
{
return Activator.CreateInstance<TContext>() as TContext;
}
private void InitDataContext()
{
m_context = CreateContext();
}
为什么我们需要为每个方法创建一个新的 DataContext 实例?原因是 DataContext 中的缓存策略。如果您创建一个新的 DataContext 实例并用它从数据库查询数据,然后更改其值并使用同一个实例执行相同的查询,DataContext 将返回存储在内部缓存中的数据,而不是将行重新映射到表。有关更多信息,请参阅《LINQ in Action》。
因此,最佳实践是为每个操作创建一个新的 DataContext 实例。不要担心性能,DataContext 是一个轻量级的资源。