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

LINQ 离线模式的 ASP.NET 数据层基类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (20投票s)

2008年10月7日

CPOL

6分钟阅读

viewsIcon

83323

downloadIcon

1134

使用此抽象类快速轻松地实现您的 LINQ 数据层

引言

LINQ to SQL 是 .NET 3.0 的一项绝佳补充!它为 .NET 应用程序中的数据访问提供了一种类型安全、强大且极其灵活的实现方式。

然而,在多层数据库应用程序中使用 LINQ to SQL 并不像直接使用那么简单。本文将介绍使用 LINQ to SQL 实现数据层时常见的陷阱,并提供一种简单、方便且灵活的方法来规避其中大多数问题。

本文提供的 LINQ to SQL 数据库抽象层 (DAL) 的通用基类具有以下特性:

  • 实现了 Repository 模式,让您能够方便地为每个 LINQ 实体类型编写少于十行代码的 CRUD (Create, Update, Delete) 操作。
  • 在离线 LINQ 模式下无缝工作。
  • 支持在一次数据库往返中,透明地更新 LINQ 实体层次结构。
  • 作为一个便利功能,它还会在调试应用程序时,将所有执行的 SQL 语句写入输出控制台。

必备组件

本文假设您对 LINQ to SQL (也称为 DLINQ) 的功能及其使用方法有基本了解。如果您不了解,请先阅读 本教程,然后再返回此页面学习如何在多层应用程序中使用 LINQ to SQL。

问题

如果您直接将 UI 层与数据库通过 LINQDataSource 对象连接起来,LINQ to SQL 将会非常容易使用。但这种方式并不怎么面向对象,肯定也不是一个推荐的架构。除非您正在编写一个快速粗糙的应用程序,并且不打算长期扩展它。

相反,大多数开发人员会将应用程序划分为多个层,例如以下结构:

  • 数据访问层
  • 业务层
  • UI 层

这被称为多层数据库应用程序设计。LINQ to SQL 将用于数据访问层。

LINQ to SQL 的问题在于——尽管它有许多优点——但在实现数据层时,它并不那么容易使用。

请看以下数据库模式:

entities.png

只要您将 LINQ 实体加载到同一个数据上下文实例并从中保存(这称为“联机模式”),使用 LINQ 实现数据层就会非常直接。

例如,我们从数据库中获取 ID 为 1 的客户实体,将其名字更改为“Homer”,然后将其保存回数据库。在多层数据库应用程序中,这段代码可能位于 UI 或业务层,并且会像这样:

//create a new repository instance
CustomersRepository customersRepository = new CustomersRepository();

//load a customer instance and change its FirstName
Customer customer = customersRepository.Load(2);
customer.FirstName = "Homer";

//commit customer to database
customersRepository.Save(customer);

实现上面使用的 LoadSave 数据层函数的简便方法是这样的:

static DataClassesDataContext context=new DataClassesDataContext();
        
public Customer Load(int CustomerID)
{
    return context.Customers.Single(c => c.ID == CustomerID);
}

public void Save(Customer toSave)
{
    context.SubmitChanges();
}

这种方法使用联机 LINQ 模式:数据上下文永远不会超出范围,因此总可以重用它来将仍然连接到它的实体保存到数据库。

诚然,这很方便,并且对于上面孤立的示例有效。但是,它存在严重的并发问题,因为一个数据库上下文用于所有数据库操作:调用 Save() 时,SubmitChanges 会提交所有已更改的实体,而不仅仅是 Save 方法在 toSave 参数中接收的 LINQ 实体。

但是,即使撇开这个缺点不谈,在多层 ASP.NET 应用程序中使用 LINQ 时,您也无法以同样的方式实现数据层。在这种情况下,您的 LINQ 实体很可能是在页面请求中加载的,然后在下一个页面请求中进行更新并保存到数据库。在此期间,您原来的数据上下文已经超出了范围,使得您的 LINQ 实体处于断开连接状态。

而且,在许多其他场景下都需要使用断开连接的 LINQ 模式:例如,您可能希望将数据库层实现为 Web 服务,提交先前序列化的 LINQ 实体到您的数据库,等等。

使用断开连接的 LINQ 实现数据层

那么,如何实现一个可以在断开连接的 LINQ 模式下工作的 Save() 数据层方法呢?

我们必须:

  1. 将实体从旧的数据上下文中分离
  2. 创建一个新的数据上下文
  3. 将实体附加到新上下文中
  4. 提交更改

在源代码中,它看起来像这样:

public Customer Load(int CustomerID)
{          
    DataClassesDataContext context  =new DataClassesDataContext();
    return context.Customers.Single(c => c.ID == CustomerID);
}

public void Save(Customer toSave)
{
    //the old data context is no more, we need to create a new one
    DataClassesDataContext context = new DataClassesDataContext();
    //serialize and deserialize the entity to detach it from the 
    //old data context. This is not part of .NET, I am calling
    //my own code here
    toSave = EntityDetacher<Customer>.Detach(toSave);
    //is the entity new or just updated?
    //ID is the customer table's identity column, so new entities should
    //have an ID == 0
    if (toSave.ID==0)
    {
        //insert entity into Customers table
        context.Customers.InsertOnSubmit(toSave);
    }
    else
    {
        //attach entity to Customers table and mark it as "changed"
        context.Customers.Attach(toSave,true);
    }
}

现在,您可以加载和修改任意数量的实体,并且只提交其中一些到数据库。但是,由于使用了断开连接的 LINQ,这种实现没有考虑 LINQ 实体之间的关联。

例如,假设您想在业务层或 UI 层中执行以下操作:

//load currently selected customer from database
Customer customer = new CustomersRepository().Load(1);

//change the customer's first name
customer.FirstName = "Homer";

//add a new bill with two billingitems to the customer
Bill newbill = new Bill
{
    Date = DateTime.Now,
    BillingItems =
        {
            new BillingItem(){ItemPrice=10, NumItems=2},
            new BillingItem(){ItemPrice=15, NumItems=1}
        }
};
customer.Bills.Add(newbill);

//create a new provider to simulate new ASP.NET page request
// save the customer
new CustomersRepository().Save(customer);

上面断开连接模式的 Save() 方法将提交对 FirstName 列的更改,但会忽略新的账单和账单项。为了使其正常工作,我们还需要递归地 AttachInsert 所有关联的子实体。

public void Save(Customer toSave)
{
    //the old data context is no more, we need to create a new one
    DataClassesDataContext context = new DataClassesDataContext();
    //serialize and deserialize the entity to detach it from the 
    //old data context. This is not part of .NET, I am calling
    //my own code here
    toSave = EntityDetacher<customer>.Detach(toSave);
    //is the entity new or just updated?
    //ID is the customer table's identity column, so new entities should
    //have an ID == 0
    if (toSave.ID==0)
    {
        //insert entity into Customers table
        context.Customers.InsertOnSubmit(toSave);
    }
    else
    {
        //attach entity to Customers table and mark it as "changed"
        context.Customers.Attach(toSave,true);
    }

    //attach or save all "bill" child entities
    foreach (Bill bill in toSave.Bills)
    {
        if (bill.ID == 0)
        {                    
            context.Bills.InsertOnSubmit(bill);
        }
        else
        {                 
            context.Bills.Attach(bill, true);
        }
        //attach or save all "BillingItem" child entities                 
        foreach (BillingItem billingitem in bill.BillingItems)
        {
            if (bill.ID == 0)
            {                 
                context.BillingItems.InsertOnSubmit(billingitem);
            }
            else
            {                     
                context.BillingItems.Attach(billingitem, true);
            }                    
        }
    }
}

这并不复杂,但那只是针对一个简单的数据库模式和一个实体类型。想象一下,如果您正在为几十种实体类型实现数据库层,并且有几十个外键关系。您将不得不为每个需要 DAL Repository 类的 LINQ 实体编写几十个嵌套的 foreach 循环。这不仅乏味,而且容易出错。每当您添加一个新表时,您都需要向各种 DAL Repository 类添加几十个 foreach 循环。

解决方案:RepositoryBase

我实现了一个名为 RepositoryBase 的类,您可以使用它来快速实现适用于上述示例的数据层。

为了使用它,您必须首先指示对象关系映射器生成可序列化的 LINQ 实体:在 Visual Studio 中打开您的 DBML 文件,在空白区域单击鼠标左键,然后在“属性”面板中将“序列化模式”设置为“单向”。

orm_properties.png

现在,您可以从 RepositoryBase 派生来实现自己的 Repository:

public class CustomersRepository : 
    //derive from RepositoryBase with the entity name and
    //data context as generic parameters
    DeverMind.RepositoryBase<Customer, DataClassesDataContext>
{
    override protected Expression<Func<Customer, bool>> GetIDSelector(int ID)
    {
        //ID needs to be the entity's ID column name
        return (Item) => Item.ID == ID;
    }       
}

public partial class Customer
{
    public static RepositoryBase<Customer,DataClassesDataContext> CreateRepository()
    {
        //create and return an instance of this entity type's repository
        return new CustomersRepository();
    }
}

为您的每种实体类型执行此操作,您就拥有了一个在断开连接模式下无缝工作的数据层。您的派生 Repository 类将自动实现以下方法:

ProviderBase-Interface.png

作为小福利,在调试应用程序时,您还可以通过 ProviderBase 在调试输出控制台中看到针对数据库运行的 SQL 命令。

没有免费午餐……

Load 操作没有显著的性能损失,但调用 SaveDelete 方法时,后台会进行一些反射操作。

对于绝大多数 DAL 需求,这可能不会对您的应用程序产生显著影响。然而,如果您执行大量更新/插入/删除操作,特别是涉及大量嵌套子实体时,您可能希望为这些子应用程序的 Repository 类编写自己的 Save / Delete 函数,如上所述。所有 Save / Delete 函数都是 virtual 的,因此您可以轻松地重写它们。

另外,请注意 RepositoryBase 不支持带循环依赖的递归保存或删除操作。

结论

本文和随附的源代码提供了一种简单、方便且可扩展的方法来实现您的多层 LINQ 数据层 CRUD 方法。它可以在断开连接模式下工作,并支持嵌套子实体的保存和加载。在保存和加载操作上存在轻微的性能损失,但您可以为这些性能至关重要的 Repository 重写它们。对于其他所有情况,只需几行代码即可搞定。

如果您有任何疑问,请告诉我。此外,欢迎随时访问我的 博客,了解更多开发文章。

版本历史

  • 2008 年 10 月 7 日 - V.0.1
    • 首次发布
  • 2008 年 2 月 26 日 - V.0.2
    • RepositoryBase 现在会更新已保存实体的 ID 和版本属性。
    • 增加了对多个 ID 列的支持。

谢谢!

感谢 Kris Vandermotten 提供的便捷 DebuggerWriter 组件,RepositoryBase 使用它来输出 SQL 调试信息。

© . All rights reserved.