65.9K
CodeProject 正在变化。 阅读更多。
Home

LINQ to SQL 的通用基类

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (8投票s)

2008年7月29日

CPOL

4分钟阅读

viewsIcon

67181

downloadIcon

451

一个用于 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> 字段,我们可以通过调用 FieldInfoSetValue 将它们的值设置为 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 是一个轻量级的资源。

© . All rights reserved.