一步一步学Linq to sql(二)
 

2009-05-19 作者:lovecherry 来源:lovecherry的blog

 

(六):探究特性

延迟执行

       IQueryable query = from c in ctx.Customers select c;

这样的查询句法不会导致语句立即执行,它仅仅是一个描述,对应一个SQL。仅仅在需要使用的时候才会执行语句,比如:

        IQueryable query = from c in ctx.Customers select c;

        foreach (Customer c in query)

            Response.Write(c.CustomerID);

如果你执行两次foreach操作,将会捕获到两次SQL语句的执行:

        IQueryable query = from c in ctx.Customers select c;

        foreach (Customer c in query)

            Response.Write(c.CustomerID);

        foreach (Customer c in query)

            Response.Write(c.ContactName);

 对应SQL

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]

FROM [dbo].[Customers] AS [t0]

 

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]

FROM [dbo].[Customers] AS [t0]

对于这样的需求,建议你先使用ToList()等方法把查询结果先进行保存,然后再对集合进行查询:

        IEnumerable<Customer> customers = (from c in ctx.Customers select c).ToList();

        foreach (Customer c in customers)

            Response.Write(c.CustomerID);

        foreach (Customer c in customers)

            Response.Write(c.ContactName);

延迟执行的优点在于我们可以像拼接SQL那样拼接查询句法,然后再执行:

        var query = from c in ctx.Customers select c;

        var newquery = (from c in query select c).OrderBy(c => c.CustomerID);

DataLoadOptions

          var products = from p in ctx.Products select p;

        foreach (var p in products)

        {

            if (p.UnitPrice > 10)

                ShowDetail(p.Order_Details);

        }

 

    private void ShowDetail(EntitySet<Order_Detail> orderdetails)

    {}

 

由于ShowDetail方法并没有使用到订单详细信息,所以这个操作只会执行下面的SQL

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued]

FROM [dbo].[Products] AS [t0]

 现在修改一下ShowDetail方法:

    private void ShowDetail(EntitySet<Order_Detail> orderdetails)

    {

        foreach (var o in orderdetails)

       {

           Response.Write(o.Quantity + "<br>");

       }

    }

你会发现Linq to sql对每个价格大于10的产品都根据产品号进行了一次查询:

SELECT [t0].[OrderID], [t0].[ProductID], [t0].[UnitPrice], [t0].[Quantity], [t0].[Discount]

FROM [dbo].[Order Details] AS [t0]

WHERE [t0].[ProductID] = @p0

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [1]

 这样的语句查询了N次。这样的查询不是很合理,我们可以通过设置DataContextDataLoadOption,来指示 DataContext再加载产品信息的同时把对应的产品订单信息一起加载:

        DataLoadOptions options = new DataLoadOptions();

        options.LoadWith<Product>(p => p.Order_Details);

        ctx.LoadOptions = options;

 var products = from p in ctx.Products select p;

。。。。。。。。

再执行先前的查询会发现Linq to sql进行了左连接:

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued], [t1].[OrderID], [t1].[ProductID] AS [ProductID2], [t1].[UnitPrice] AS [UnitPrice2], [t1].[Quantity], [t1].[Discount], (

    SELECT COUNT(*)

    FROM [dbo].[Order Details] AS [t2]

    WHERE [t2].[ProductID] = [t0].[ProductID]

    ) AS [count]

FROM [dbo].[Products] AS [t0]

LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[ProductID] = [t0].[ProductID]

ORDER BY [t0].[ProductID], [t1].[OrderID]

那么,我们怎么限制订单详细表的加载条件那?

DataLoadOptions options = new DataLoadOptions();

options.LoadWith<Product>(p => p.Order_Details);

options.AssociateWith<Product>(p => p.Order_Details.Where(od => od.Quantity > 80));

ctx.LoadOptions = options;

var products = from p in ctx.Products select p;

这样,就只会有数量大于80的订单详细信息会和产品一起加载:

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued], [t1].[OrderID], [t1].[ProductID] AS [ProductID2], [t1].[UnitPrice] AS [UnitPrice2], [t1].[Quantity], [t1].[Discount], (

    SELECT COUNT(*)

    FROM [dbo].[Order Details] AS [t2]

    WHERE ([t2].[Quantity] > @p0) AND ([t2].[ProductID] = [t0].[ProductID])

    ) AS [count]

FROM [dbo].[Products] AS [t0]

LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON ([t1].[Quantity] > @p0) AND ([t1].[ProductID] = [t0].[ProductID])

ORDER BY [t0].[ProductID], [t1].[OrderID]

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [80]

DataLoadOptions限制 

Linq to sqlDataLoadOptions的使用是有限制的,它只支持11对多的关系。一个顾客可能有多个订单,一个订单可能有多个详细订单:

        DataLoadOptions options = new DataLoadOptions();

        options.LoadWith<Customer>(c => c.Orders);

        options.LoadWith<Order>(o => o.Order_Details);

        ctx.LoadOptions = options;

        IEnumerable<Customer> customers = ctx.Customers.ToList<Customer>();

 

 这样的语句执行后会导致下面的SQL执行N次(参数不同):

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry], [t1].[OrderID] AS [OrderID2], [t1].[ProductID], [t1].[UnitPrice], [t1].[Quantity], [t1].[Discount], (

    SELECT COUNT(*)

    FROM [dbo].[Order Details] AS [t2]

    WHERE [t2].[OrderID] = [t0].[OrderID]

    ) AS [count]

FROM [dbo].[Orders] AS [t0]

LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID]

WHERE [t0].[CustomerID] = @x1

ORDER BY [t0].[OrderID], [t1].[ProductID]

-- @x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [ALFKI]

 

而对于多对1的关系,Linq to sql对于DataLoadOptions没有限制:

        DataLoadOptions options = new DataLoadOptions();

        options.LoadWith<Product>(c => c.Category);

        options.LoadWith<Product>(c => c.Order_Details);

        options.LoadWith<Order_Detail>(o => o.Order);

        ctx.LoadOptions = options;

        IEnumerable<Product> products = ctx.Products.ToList<Product>();

由于多个产品对应1个分类,多个详细订单对应1个订单,只有产品和详细订单才是多对1的关系,所以也只会有1SQL(不过这样的操作还是少执行为妙,消耗太大了):

SELECT [t0].[ProductID], [t0].[ProductName], [t0].[SupplierID], [t0].[CategoryID], [t0].[QuantityPerUnit], [t0].[UnitPrice], [t0].[UnitsInStock], [t0].[UnitsOnOrder], [t0].[ReorderLevel], [t0].[Discontinued], [t3].[OrderID], [t3].[ProductID] AS [ProductID2], [t3].[UnitPrice] AS [UnitPrice2], [t3].[Quantity], [t3].[Discount], [t4].[OrderID] AS [OrderID2], [t4].[CustomerID], [t4].[EmployeeID], [t4].[OrderDate], [t4].[RequiredDate], [t4].[ShippedDate], [t4].[ShipVia], [t4].[Freight], [t4].[ShipName], [t4].[ShipAddress], [t4].[ShipCity], [t4].[ShipRegion], [t4].[ShipPostalCode], [t4].[ShipCountry], (

    SELECT COUNT(*)

    FROM [dbo].[Order Details] AS [t5]

    INNER JOIN [dbo].[Orders] AS [t6] ON [t6].[OrderID] = [t5].[OrderID]

    WHERE [t5].[ProductID] = [t0].[ProductID]

    ) AS [count], [t2].[test], [t2].[CategoryID] AS [CategoryID2], [t2].[CategoryName], [t2].[Description], [t2].[Picture]

FROM [dbo].[Products] AS [t0]

LEFT OUTER JOIN (

    SELECT 1 AS [test], [t1].[CategoryID], [t1].[CategoryName], [t1].[Description], [t1].[Picture]

    FROM [dbo].[Categories] AS [t1]

    ) AS [t2] ON [t2].[CategoryID] = [t0].[CategoryID]

LEFT OUTER JOIN ([dbo].[Order Details] AS [t3]

    INNER JOIN [dbo].[Orders] AS [t4] ON [t4].[OrderID] = [t3].[OrderID]) ON [t3].[ProductID] = [t0].[ProductID]

ORDER BY [t0].[ProductID], [t2].[CategoryID], [t3].[OrderID]

主键缓存

Linq to sql对查询过的对象进行缓存,之后的如果只根据主键查询一条记录的话会直接从缓存中读取。比如下面的代码:

        Customer c1 = ctx.Customers.Single(customer => customer.CustomerID == "ANATR");

        c1.ContactName = "zhuye";

        Customer c2 = ctx.Customers.Single(customer => customer.CustomerID == "ANATR");

        Response.Write(c2.ContactName);

执行后只会产生一条SQL

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]

FROM [dbo].[Customers] AS [t0]

WHERE [t0].[CustomerID] = @p0

-- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [ANATR]

由于没有提交修改,所以数据库中的记录还是没有更新。由于这个特性,我们在使用存储过程作为实体更新方法的时候就要当心了,存储过程书写错误,即使你提交了修改也很可能导致缓存中的数据和数据库中的数据不一致,引起不必要的麻烦。

DataContext隔离

有的时候我们会把对象从外部传入DataContext,要求它更新,由于不同的DataContext是相对独立的。由于新的DataContext中还没有获取实体,我们只能通过附加方式更新数据。

首先把Customer表的主键字段加上IsVersion标识:

    [Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true, IsVersion = true)]

运行下面的测试代码:

        Customer c = new Customer { CustomerID = "ALFKI", ContactName = "zhuye", CompanyName = "1111" };

        ctx.Customers.Attach(c, true);

        ctx.SubmitChanges();

会捕捉到下面的SQL语句:

UPDATE [dbo].[Customers]

SET [CompanyName] = @p2, [ContactName] = @p3, [ContactTitle] = @p4, [Address] = @p5, [City] = @p6, [Region] = @p7, [PostalCode] = @p8, [Country] = @p9, [Phone] = @p10, [Fax] = @p11

WHERE ([CustomerID] = @p0) AND ([CustomerID] = @p1)

-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [ALFKI]

-- @p1: Input String (Size = 5; Prec = 0; Scale = 0) [ALFKI]

-- @p2: Input String (Size = 4; Prec = 0; Scale = 0) [1111]

-- @p3: Input String (Size = 5; Prec = 0; Scale = 0) [zhuye]

-- @p4: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p5: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p6: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p7: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p8: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p9: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p10: Input String (Size = 0; Prec = 0; Scale = 0) []

-- @p11: Input String (Size = 0; Prec = 0; Scale = 0) []

(七):并发与事务

检测并发

首先使用下面的SQL语句查询数据库的产品表:

select * from products where categoryid=1

查询结果如下图:

为了看起来清晰,我已经事先把所有分类为1产品的价格和库存修改为相同值了。然后执行下面的程序:

        var query = from p in ctx.Products where p.CategoryID == 1 select p;

        foreach (var p in query)

            p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1);

        ctx.SubmitChanges(); // 在这里设断点

我们使用调试方式启动,由于设置了断点,程序并没有进行更新操作。此时,我们在数据库中运行下面的语句:

update products

set unitsinstock = unitsinstock -2, unitprice= unitprice + 1

where categoryid = 1

然后在继续程序,会得到修改并发(乐观并发冲突)的异常,提示要修改的行不存在或者已经被改动。当客户端提交的修改对象自读取之后已经在数据库中发生改动,就产生了修改并发。解决并发的包括两步,一是查明哪些对象发生并发,二是解决并发。如果你仅仅是希望更新时不考虑并发的话可以关闭相关列的更新验证,这样在这些列上发生并发就不会出现异常:

[Column(Storage="_UnitsInStock", DbType="SmallInt", UpdateCheck = UpdateCheck.Never)]

[Column(Storage="_UnitPrice", DbType="Money", UpdateCheck = UpdateCheck.Never)]

为这两列标注不需要进行更新检测。假设现在产品价格和库存分别是27和32。那么,我们启动程序(设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别是28和31。价格+1是之前更新的功劳,库存最终是-1是我们程序之后更新的功劳。当在同一个字段上(库存)发生并发冲突的时候,默认是最后的那次更新获胜。

解决并发

如果你希望自己处理并发的话可以把前面对列的定义修改先改回来,看下面的例子:

        var query = from p in ctx.Products where p.CategoryID == 1 select p;

        foreach (var p in query)

            p.UnitsInStock = Convert.ToInt16(p.UnitsInStock - 1);

        try

        {

            ctx.SubmitChanges(ConflictMode.ContinueOnConflict);

        }

        catch (ChangeConflictException)

        {

            foreach (ObjectChangeConflict cc in ctx.ChangeConflicts)

            {

                Product p = (Product)cc.Object;

                Response.Write(p.ProductID + "<br/>");

                cc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,所有更新以原先更新为准

            }

        }

        ctx.SubmitChanges();

首先可以看到,我们使用try{}catch{}来捕捉并发冲突的异常。在SubmitChanges的时候,我们选择了ConflictMode.ContinueOnConflict选项。也就是说遇到并发了还是继续。在catch{}中,我们从ChangeConflicts中获取了并发的对象,然后经过类型转化后输出了产品ID,然后选择的解决方案是RefreshMode.OverwriteCurrentValues。也就是说,放弃当前的更新,所有更新以原先更新为准。

我们来测试一下,假设现在产品价格和库存分别是27和32。那么,我们启动程序(在ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别是28和30。之前SQL语句库存-2生效了,而我们程序的更新(库存-1)被放弃了。在页面上也显示了所有分类为1的产品ID(因为我们之前的SQL语句是对所有分类为1的产品都进行修改的)。

 然后,我们来修改一下解决并发的方式:

cc.Resolve(RefreshMode.KeepCurrentValues); // 放弃原先更新,所有更新以当前更新为准

来测试一下,假设现在产品价格和库存分别是27和32。那么,我们启动程序(在ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别是27和31。产品价格没有变化,库存-1了,都是我们程序的功劳,SQL语句的更新被放弃了。

然后,我们再来修改一下解决并发的方式:

cc.Resolve(RefreshMode.KeepChanges); // 原先更新有效,冲突字段以当前更新为准

来测试一下,假设现在产品价格和库存分别是27和32。那么,我们启动程序(在ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别是28和31。这就是默认方式,在保持原先更新的基础上,对于发生冲突的字段以最后更新为准。

我们甚至还可以针对不同的字段进行不同的处理策略:

foreach (ObjectChangeConflict cc in ctx.ChangeConflicts)

{

    Product p = (Product)cc.Object;

    foreach (MemberChangeConflict mc in cc.MemberConflicts)

    {

        string currVal = mc.CurrentValue.ToString();

        string origVal = mc.OriginalValue.ToString();

        string databaseVal = mc.DatabaseValue.ToString();

        MemberInfo mi = mc.Member;

        string memberName = mi.Name;

        Response.Write(p.ProductID + " " + mi.Name + " " + currVal + " " + origVal +" "+ databaseVal + "<br/>");

        if (memberName == "UnitsInStock")

            mc.Resolve(RefreshMode.KeepCurrentValues); // 放弃原先更新,所有更新以当前更新为准

        else if (memberName == "UnitPrice")

            mc.Resolve(RefreshMode.OverwriteCurrentValues); // 放弃当前更新,所有更新以原先更新为准

        else

            mc.Resolve(RefreshMode.KeepChanges); // 原先更新有效,冲突字段以当前更新为准

    }

}

比如上述代码就对库存字段作放弃原先更新处理,对价格字段作放弃当前更新处理。我们来测试一下,假设现在产品价格和库存分别是27和32。那么,我们启动程序(在ctx.SubmitChanges(ConflictMode.ContinueOnConflict)这里设置端点),然后运行UPDATE语句,把价格+1,库存-2,然后价格和库存分别为28和30了,继续程序可以发现价格和库存分别为28和31了。说明对价格的处理确实保留了原先的更新,对库存的处理保留了当前的更新。页面上显示的结果如下图:

 

最后,我们把提交语句修改为:

ctx.SubmitChanges(ConflictMode.FailOnFirstConflict);

表示第一次发生冲突的时候就不再继续了,然后并且去除最后的ctx.SubmitChanges();语句。来测试一下,在执行了SQL后再继续程序可以发现界面上只输出了数字1,说明在第一条记录失败后,后续的并发冲突就不再处理了。

事务处理

Linq to sql在提交更新的时候默认会创建事务,一部分修改发生错误的话其它修改也不会生效:

        ctx.Customers.Add(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" });

        ctx.Customers.Add(new Customer { CustomerID = "abcde", CompanyName = "zhuye" });

        ctx.SubmitChanges();

假设数据库中已经存在顾客ID为“abcde”的记录,那么第二次插入操作失败将会导致第一次的插入操作失效。执行程序后会得到一个异常,查询数据库发现“abcdf”这个顾客也没有插入到数据库中。

 如果每次更新后直接提交修改,那么我们可以使用下面的方式做事务:

        if (ctx.Connection != null) ctx.Connection.Open();

        DbTransaction tran = ctx.Connection.BeginTransaction();

        ctx.Transaction = tran;

        try

        {

            CreateCustomer(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" });

            CreateCustomer(new Customer { CustomerID = "abcde", CompanyName = "zhuye" });

            tran.Commit();

        }

        catch

        {

            tran.Rollback();

        }

 

    private void CreateCustomer(Customer c)

    {

        ctx.Customers.Add(c);

        ctx.SubmitChanges();

    }

运行程序后发现增加顾客abcdf的操作并没有成功。或者,我们还可以通过TransactionScope实现事务:

        using (TransactionScope scope = new TransactionScope())

        {

            CreateCustomer(new Customer { CustomerID = "abcdf", CompanyName = "zhuye" });

            CreateCustomer(new Customer { CustomerID = "abcde", CompanyName = "zhuye" });

            scope.Complete();

        }

(八):继承与关系

论坛表结构

为了演示继承与关系,我们创建一个论坛数据库,在数据库中创建三个表:

1、  论坛版块分类表 dbo.Categories

字段名

字段类型

可空

备注

CategoryID

int

not null

identity/主键

CategoryName

varchar(50)

not null

 

2、  论坛版块表 dbo.Boards

字段名

字段类型

可空

备注

BoardID

int

not null

identity/主键

BoardName

varchar(50)

not null

 

BoardCategory

int

not null

对应论坛版块分类表的CategoryID

3、  论坛主题表 dbo.Topics

字段名

字段类型

可空

备注

TopicID

int

not null

identity/主键

TopicTitle

varchar(50)

not null

 

TopicContent

varchar(max)

not null

 

ParentTopic

int

null

如果帖子是主题贴这个字段为null,否则就是所属主题id

TopicType

tinyint

not null

0 – 主题贴

1 – 回复帖

实体继承的定义

Linq to sql支持实体的单表继承,也就是基类和派生类都存储在一个表中。对于论坛来说,帖子有两种,一种是主题贴,一种是回复帖。那么,我们就先定义帖子基类:

[Table(Name = "Topics")]

public class Topic

{

    [Column(Name = "TopicID", DbType = "int identity", IsPrimaryKey = true, IsDbGenerated = true, CanBeNull = false)]

    public int TopicID { get; set; }

    [Column(Name = "TopicType", DbType = "tinyint", CanBeNull = false)]

    public int TopicType { get; set; }

    [Column(Name = "TopicTitle", DbType = "varchar(50)", CanBeNull = false)]

    public string TopicTitle { get; set; }

    [Column(Name = "TopicContent", DbType = "varchar(max)", CanBeNull = false)]

    public string TopicContent { get; set; }

}

这些实体的定义大家应该很熟悉了。下面,我们再来定义两个实体继承帖子基类,分别是主题贴和回复贴:

public class NewTopic : Topic

{

    public NewTopic()

    {

        base.TopicType = 0;

    }

}

public class Reply : Topic

{

    public Reply()

    {

        base.TopicType = 1;

    }

    [Column(Name = "ParentTopic", DbType = "int", CanBeNull = false)]

    public int ParentTopic { get; set; }

}

对于主题贴,在数据库中的TopicType就保存为0,而对于回复贴就保存为1。回复贴还有一个相关字段就是回复所属主题贴的TopicID。那么,我们怎么告知Linq to sqlTopicType0的时候识别为NewTopic,而1则识别为Reply那?只需稍微修改一下前面的Topic实体定义:

[Table(Name = "Topics")]

[InheritanceMapping(Code = 0, Type = typeof(NewTopic), IsDefault = true)]

[InheritanceMapping(Code = 1, Type = typeof(Reply))]

public class Topic

{

    [Column(Name = "TopicID", DbType = "int identity", IsPrimaryKey = true, IsDbGenerated = true, CanBeNull = false)]

    public int TopicID { get; set; }

    [Column(Name = "TopicType", DbType = "tinyint", CanBeNull = false, IsDiscriminator = true)]

    public int TopicType { get; set; }

    [Column(Name = "TopicTitle", DbType = "varchar(50)", CanBeNull = false)]

    public string TopicTitle { get; set; }

    [Column(Name = "TopicContent", DbType = "varchar(max)", CanBeNull = false)]

    public string TopicContent { get; set; }

}

为类加了InheritanceMapping特性定义,0的时候类型就是NewTopic1的时候就是Reply。并且为TopicType字段上的特性中加了IsDiscriminator = true,告知Linq to sql这个字段就是用于分类的字段。

实体继承的使用

定义好继承的实体之后,我们就可以使用了。先是自定义一个DataContext吧:

public partial class BBSContext : DataContext

{

    public Table<BoardCategory> BoardCategories;

    public Table<Board> Boards;

    public Table<Topic> Topics;

    public BBSContext(string connection) : base(connection) { }

}

然后,我们来测试一下Linq to sql是否能根据TopicType识别派生类:

        BBSContext ctx = new BBSContext("server=xxx;database=BBS;uid=xxx;pwd=xxx");

        var query = from t in ctx.Topics select t;

        foreach (Topic topic in query)

        {

            if (topic is NewTopic)

            {

                NewTopic newtopic = topic as NewTopic;

                Response.Write("标题:" + newtopic.TopicTitle + " 类型:" + newtopic.TopicType + "<br/>");

            }

            else if (topic is Reply)

            {

                Reply reply = topic as Reply;

                Response.Write("标题:" + reply.TopicTitle + " 类型:" + reply.TopicType + " 隶属主题:" + reply.ParentTopic + "<br/>");

            }

        }

然后我们往Topics表中加一些数据,如下图:

 

启动程序得到如下测试结果:

 

当然,你也可以在查询句法中直接查询派生实体:

        IEnumerable newtopiclist = (from t in ctx.Topics.OfType<NewTopic>() select t).ToList();

        newtopics.DataSource = newtopiclist;

        IEnumerable replylist = (from t in ctx.Topics.OfType<Reply>() select t).ToList();

        replies.DataSource = replylist;

        Page.DataBind();

newtopicreplies是两个GridView控件,执行效果如下图:

 

再来看看如何进行增删操作:

        NewTopic nt = new NewTopic() { TopicTitle = "还是新主题", TopicContent = "还是新主题" };

        Reply rpl = new Reply() { TopicTitle = "还是新回复", TopicContent = "还是新回复", ParentTopic = 4 };

        ctx.Topics.Add(nt);

        ctx.Topics.Add(rpl);

        ctx.SubmitChanges();

        rpl = ctx.Topics.OfType<Reply>().Single(reply => reply.TopicID == 8);

        ctx.Topics.Remove(rpl);

        ctx.SubmitChanges();

实体关系的定义

比如我们的论坛分类表和论坛版块表之间就有关系,这种关系是1对多的关系。也就是说一个论坛分类可能有多个论坛版块,这是很常见的。定义实体关系的优势在于,我们无须显式作连接操作就能处理关系表的条件。

首先来看看分类表的定义:

[Table(Name = "Categories")]

public class BoardCategory

{

    [Column(Name = "CategoryID", DbType = "int identity", IsPrimaryKey = true, IsDbGenerated = true, CanBeNull = false)]

    public int CategoryID { get; set; }

    [Column(Name = "CategoryName", DbType = "varchar(50)", CanBeNull = false)]

    public string CategoryName { get; set; }

    private EntitySet<Board> _Boards;

    [Association(OtherKey = "BoardCategory", Storage = "_Boards")]

    public EntitySet<Board> Boards

    {

        get { return this._Boards; }

        set { this._Boards.Assign(value); }

    }

    public BoardCategory()

    {

        this._Boards = new EntitySet<Board>();

    }

}

CategoryIDCategoryName的映射没有什么不同,只是我们还增加了一个Boards属性,它返回的是Board实体集。通过特性,我们定义了关系外键为BoardCategoryBoard表的一个字段)。然后来看看1对多,多端版块表的实体:

[Table(Name = "Boards")]

public class Board

{

    [Column(Name = "BoardID", DbType = "int identity", IsPrimaryKey = true, IsDbGenerated = true, CanBeNull = false)]

    public int BoardID { get; set; }

    [Column(Name = "BoardName", DbType = "varchar(50)", CanBeNull = false)]

    public string BoardName { get; set; }

    [Column(Name = "BoardCategory", DbType = "int", CanBeNull = false)]

    public int BoardCategory { get; set; }

    private EntityRef<BoardCategory> _Category;

    [Association(ThisKey = "BoardCategory", Storage = "_Category")]

    public BoardCategory Category

    {

        get { return this._Category.Entity; }

        set

        {

            this._Category.Entity = value;

            value.Boards.Add(this);

        }

}

}

在这里我们需要关联分类,设置了Category属性使用BoardCategory字段和分类表关联。 

实体关系的使用

好了,现在我们就可以在查询句法中直接关联表了(数据库中不一定要设置表的外键关系):

        Response.Write("-------------查询分类为1的版块-------------<br/>");

        var query1 = from b in ctx.Boards where b.Category.CategoryID == 1 select b;

        foreach (Board b in query1)

            Response.Write(b.BoardID + " " + b.BoardName + "<br/>");

        Response.Write("-------------查询版块大于2个的分类-------------<br/>");

        var query2 = from c in ctx.BoardCategories where c.Boards.Count > 2 select c;

        foreach (BoardCategory c in query2)

            Response.Write(c.CategoryID + " " + c.CategoryName + " " + c.Boards.Count + "<br/>");

在数据库中加一些测试数据,如下图:

 

运行程序后得到下图的结果:

 

我想定义实体关系的方便我不需要再用语言形容了吧。执行上述的程序会导致下面SQL的执行:

SELECT [t0].[BoardID], [t0].[BoardName], [t0].[BoardCategory]

FROM [Boards] AS [t0]

INNER JOIN [Categories] AS [t1] ON [t1].[CategoryID] = [t0].[BoardCategory]

WHERE [t1].[CategoryID] = @p0

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [1]

SELECT [t0].[CategoryID], [t0].[CategoryName]

FROM [Categories] AS [t0]

WHERE ((

    SELECT COUNT(*)

    FROM [Boards] AS [t1]

    WHERE [t1].[BoardCategory] = [t0].[CategoryID]

    )) > @p0

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [2]

SELECT [t0].[BoardID], [t0].[BoardName], [t0].[BoardCategory]

FROM [Boards] AS [t0]

WHERE [t0].[BoardCategory] = @p0

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [1]

可以看到,第二个查询并没有做外连接,还记得DataLoadOptions吗?我们可以要求Linq to sql在读取版块分类信息的时候也把版块信息一起加载:

DataLoadOptions options = new DataLoadOptions();

        options.LoadWith<BoardCategory>(c => c.Boards);

        ctx.LoadOptions = options;

        Response.Write("-------------查询版块大于2个的分类-------------<br/>");

        var query2 = from c in ctx.BoardCategories where c.Boards.Count > 2 select c;

        foreach (BoardCategory c in query2)

            Response.Write(c.CategoryID + " " + c.CategoryName + " " + c.Boards.Count + "<br/>");

查询经过改造后会得到下面的SQL:

SELECT [t0].[CategoryID], [t0].[CategoryName], [t1].[BoardID], [t1].[BoardName], [t1].[BoardCategory], (

    SELECT COUNT(*)

    FROM [Boards] AS [t3]

    WHERE [t3].[BoardCategory] = [t0].[CategoryID]

    ) AS [count]

FROM [Categories] AS [t0]

LEFT OUTER JOIN [Boards] AS [t1] ON [t1].[BoardCategory] = [t0].[CategoryID]

WHERE ((

    SELECT COUNT(*)

    FROM [Boards] AS [t2]

    WHERE [t2].[BoardCategory] = [t0].[CategoryID]

    )) > @p0

ORDER BY [t0].[CategoryID], [t1].[BoardID]

-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [2]

在添加分类的时候,如果这个分类下还有新的版块,那么提交新增分类的时候版块也会新增:

        BoardCategory dbcat = new BoardCategory() { CategoryName = "Database" };

        Board oracle = new Board() { BoardName = "Oracle", Category = dbcat};

        ctx.BoardCategories.Add(dbcat);

        ctx.SubmitChanges();

上述代码导致下面的SQL被执行:

INSERT INTO [Categories]([CategoryName]) VALUES (@p0)

SELECT [t0].[CategoryID]

FROM [Categories] AS [t0]

WHERE [t0].[CategoryID] = (SCOPE_IDENTITY())

-- @p0: Input AnsiString (Size = 8; Prec = 0; Scale = 0) [Database]

INSERT INTO [Boards]([BoardName], [BoardCategory]) VALUES (@p0, @p1)

SELECT [t0].[BoardID]

FROM [Boards] AS [t0]

WHERE [t0].[BoardID] = (SCOPE_IDENTITY())

-- @p0: Input AnsiString (Size = 6; Prec = 0; Scale = 0) [Oracle]

-- @p1: Input Int32 (Size = 0; Prec = 0; Scale = 0) [23]

(九):其它补充

外部映射文件

我们可以使用sqlmetal命令行工具来生成外部映射文件,使用方法如下:

1、开始菜单 -》 VS2008 -》VS工具 -》VS2008命令行提示

2、输入命令:

D:\Program Files\Microsoft Visual Studio 9.0\VC>sqlmetal /conn:server=xxx;

database=Northwind;uid=xxx;pwd=xxx /map:c:\northwind.map /code:c:\northwind.cs

3、这样,我们就可以在C盘下得到一个xml映射文件和C#的实体类代码

4、把.cs文件添加到项目中来(放到App_Code目录),然后使用下面的代码加载映射文件:

String path = @"C:\Northwind.map";

XmlMappingSource xms = XmlMappingSource.FromXml(File.ReadAllText(path));

Northwind ctx = new Northwind("server=xxx;database=Northwind;uid=xxx;pwd=xxx", xms);

5、现在就可以照常进行其它工作了。使用sqlmetal可以很方便的同步数据库与实体和映射文件。每次修改数据库结构,从dbml设计器上删除表、存储过程然后再重新添加也是很麻烦的事情。

处理空值

var count = (from c in ctx.Customers where c.Region == null select c).Count();

Response.Write(count + "<br/>");

var query = from emp in ctx.Employees select emp.ReportsTo;

foreach (Nullable<int> r in query)

{

  Response.Write(r.HasValue ? r.Value.ToString() + "<br/>" : "没有<br/>");

}

代码执行后捕获到下面的SQL被执行:

SELECT COUNT(*) AS [value]

FROM [dbo].[Customers] AS [t0]

WHERE [t0].[Region] IS NULL

SELECT [t0].[ReportsTo]

FROM [dbo].[Employees] AS [t0]

已编译查询

对于一些在项目中经常被用到的查询可以封装成已编译查询,这样就能提高执行效率:

static class Queries

{

    public static Func<NorthwindDataContext, string, IQueryable<Customer>>

        CustomersByCity = CompiledQuery.Compile((NorthwindDataContext ctx, string city) => from c in ctx.Customers where c.City == city select c);

}

调用查询方式如下:   

GridView1.DataSource = Queries.CustomersByCity(ctx, "London");

GridView1.DataBind();

获取一些信息

  var query = from c in ctx.Customers select c;

  Response.Write("Provider类型:" + ctx.Mapping.ProviderType + "<br/>");

  Response.Write("数据库:" + ctx.Mapping.DatabaseName + "<br/>");

  Response.Write("表:" + ctx.Mapping.GetTable(typeof(Customer)).TableName + "<br/>");

  Response.Write("表达式:" + query.Expression.ToString() + "<br/>");

  Response.Write("sql:" + query.Provider.ToString() + "<br/>");

上面的代码执行结果如下:

Provider类型:System.Data.Linq.SqlClient.SqlProvider
数据库:Northwind
表:dbo.Customers
表达式:Table(Customer).Select(c => c)
sql:SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax] FROM [dbo].[Customers] AS [t0]

撤销提交

 var customer = ctx.Customers.Single(c => c.CustomerID == "AROUT");

 customer.ContactName = "zhuye";

 customer.Country = "Shanghai";

 Response.Write(string.Format("Name:{0},Country:{1}<br/>", customer.ContactName, customer.Country));

 customer = ctx.Customers.GetOriginalEntityState(customer);

 Response.Write(string.Format("Name:{0},Country:{1}<br/>", customer.ContactName, customer.Country));

上面的代码执行效果如下:

Name:zhuye,Country:Shanghai
Name:Thomas Hardy,Country:UK

批量操作 

下面的代码会导致提交N次DELETE操作:

var query = from c in ctx.Customers select c;

ctx.Customers.RemoveAll(query);

ctx.SubmitChanges();

应该使用sql语句进行批操作:

 string sql = String.Format("delete from {0}", ctx.Mapping.GetTable(typeof(Customer)).TableName);

  ctx.ExecuteCommand(sql);

对于批量更新操作也是同样道理。

(十):分层构架的例子

项目介绍

这节将要把《一步一步学Linq to sql(三):增删改》中留言簿的例子修改为使用WCF的多层构架。我们将会建立以下项目:

  • A,网站项目 WebSite:留言簿表现层
  • B,类库项目 Contract:定义数据访问服务的契约
  • C,类库项目 Service:定义数据访问服务
  • D,类库项目Entity:留言簿实体
  • E,控制台项目Host:承载数据访问服务

项目之间的引用如下:

  • A引用BD
  • B引用DSystem.ServiceModel程序集
  • C引用BDSystem.ServiceModel以及System.Data.Linq程序集
  • D引用System.Data.Linq程序集
  • E引用CSystem.ServiceModel程序集

生成映射文件和实体

打开VS2008命令行提示,执行以下命令:

sqlmetal /conn:server=xxx;database=GuestBook;uid=xxx;pwd=xxx /map:c:\guestbook.map /code:c:\guestbook.cs /serialization:Unidirectional

注意到,这里我们使用了serialization开关,告知sqlmetal在生成实体的时候自动把它们标记为WCF数据对象。生成结束后把C:\GUESTBOOK.CS添加到Entity项目中。

编写数据访问服务

首先我们可以定义出留言簿数据访问服务的契约(接口),把如下的代码保存为IDataAccess.cs放在Contract类库项目中:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ServiceModel;

namespace Contract

{

    [ServiceContract]

    public interface IDataAccess

    {

        [OperationContract]

        void SendMessage(TbGuestBook gb);

        [OperationContract]

        List<TbGuestBook> GetData();

        [OperationContract]

        void DeleteMessage(string ID);

        [OperationContract]

        void SendReply(TbGuestBook gb);

    }

}

在这里定义了四个方法:

  • 创建留言
  • 获取所有留言
  • 删除留言
  • 管理员发表回复

然后,我们来实现这个契约,把如下代码保存为DataAccess.cs放在Service类库项目中:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Contract;

using System.Data.Linq.Mapping;

using System.IO;

using System.ServiceModel;

namespace Service

{

    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]

    public class DataAccess : IDataAccess

    {

        GuestBook ctx;

        public DataAccess()

        {

            XmlMappingSource xms = XmlMappingSource.FromXml(File.ReadAllText("c:\\guestbook.map"));

            ctx = new GuestBook("server=srv-devdbhost;database=GuestBook;uid=sa;pwd=Abcd1234", xms);

            ctx.Log = Console.Out;

        }

        public void SendMessage(TbGuestBook gb)

        {

            ctx.TbGuestBook.Add(gb);

            ctx.SubmitChanges();

        }

        public List<TbGuestBook> GetData()

        {

            var query = from gb in ctx.TbGuestBook orderby gb.PostTime descending select gb;

            return query.ToList();

        }

        public void DeleteMessage(string ID)

        {

            TbGuestBook gb = ctx.TbGuestBook.Single(message => message.ID == new Guid(ID));

            ctx.TbGuestBook.Remove(gb);

            ctx.SubmitChanges();

        }

        public void SendReply(TbGuestBook gb)

        {

           //ctx.ExecuteCommand("update tbGuestBook set reply={0},isreplied=1 where ID={1}", gb.Reply, gb.ID);

            TbGuestBook record = ctx.TbGuestBook.Single(message => message.ID == gb.ID);

            record.IsReplied = true;

            record.Reply = gb.Reply;

            ctx.SubmitChanges();

        }

    }

}

这里需要注意几点:

  • 我们把DataContext的操作在控制台输出
  • 在进行发表回复(更新操作)的时候,注释的代码和没有注释的代码虽然都能完成更新操作,但是前者更合理,因为后者会先进行SELECT再进行UPDATE

WCF服务端与客户端

打开Host项目中的Program.cs,使用下面的代码来实现WCF的服务端:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ServiceModel;

using Service;

using Contract;

namespace Host

{

    class Program

    {

        static void Main(string[] args)

        {

            Uri uri = new Uri("net.tcp://localhost:8080/DataAccessService");

            using (ServiceHost sh = new ServiceHost(typeof(DataAccess), uri))

            {

                NetTcpBinding ctb = new NetTcpBinding();

                sh.AddServiceEndpoint(typeof(IDataAccess), ctb, string.Empty);

                sh.Opened += delegate { Console.WriteLine("服务已经启动"); };

                sh.Open();

                Console.ReadLine();

            }

        }

    }

}

WebSite项目中的App_Code文件夹下创建一个用户调用服务的类,GetService.cs

using System;

using System.Data;

using System.Configuration;

using System.Linq;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

using System.Xml.Linq;

using Contract;

using System.ServiceModel.Description;

using System.ServiceModel;

public class GetService

{

    public static IDataAccess GetDataAccessService()

    {

        ServiceEndpoint sep = new ServiceEndpoint(ContractDescription.GetContract(typeof(IDataAccess)),

            new NetTcpBinding(),

            new EndpointAddress("net.tcp://localhost:8080/DataAccessService"));

        ChannelFactory<IDataAccess> cf = new ChannelFactory<IDataAccess>(sep);

        return cf.CreateChannel();

    }

}

调用服务

最后,就可以调用数据访问服务来进行留言、回复、删除留言等操作了。页面的代码不再贴了,大家可以看第三篇或者下载源代码。我们把Default.cs修改成如下:

public partial class _Default : System.Web.UI.Page

{

    protected void Page_Load(object sender, EventArgs e)

    {

        if (!IsPostBack)

        {

            SetBind();

        }

    }

    protected void btn_SendMessage_Click(object sender, EventArgs e)

    {

        TbGuestBook gb = new TbGuestBook();

        gb.ID = Guid.NewGuid();

        gb.IsReplied = false;

        gb.PostTime = DateTime.Now;

        gb.UserName = tb_UserName.Text;

        gb.Message = tb_Message.Text;

        GetService.GetDataAccessService().SendMessage(gb);

        SetBind();

    }

    private void SetBind()

    {

        rpt_Message.DataSource = GetService.GetDataAccessService().GetData();

        rpt_Message.DataBind();

    }

}

Admin.cs代码修改成如下:

public partial class Admin : System.Web.UI.Page

{

    protected void Page_Load(object sender, EventArgs e)

    {

        if (!IsPostBack)

        {

            SetBind();

        }

    } 

    private void SetBind()

    {

        rpt_Message.DataSource = GetService.GetDataAccessService().GetData();

        rpt_Message.DataBind();

    }

    protected void rpt_Message_ItemCommand(object source, RepeaterCommandEventArgs e)

    {

        if (e.CommandName == "DeleteMessage")

        {

            GetService.GetDataAccessService().DeleteMessage(e.CommandArgument.ToString());

            SetBind();

        }

        if (e.CommandName == "SendReply")

        {

            TbGuestBook gb = new TbGuestBook();

            gb.ID = new Guid(e.CommandArgument.ToString());

            gb.Reply = ((TextBox)e.Item.FindControl("tb_Reply")).Text;

            GetService.GetDataAccessService().SendReply(gb);

            SetBind();

        }

    }

}

就这样实现了一个多层构架的留言簿程序。对于WCF的一些内容本文不多作解释了。点击这里下载本篇代码。
        
如果您觉得这个例子太简单,还可以在这里下载一个Linq/WCF/MVC结合使用更复杂的例子,此例的目的主要演示一个框架,实现不完整。  

一步一步学Linq to sql到这里就结束了,看到这里应该已经算师父领进门了,后续的提高还要靠大家自己去琢磨。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织