(六):探究特性
延迟执行
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次。这样的查询不是很合理,我们可以通过设置DataContext的DataLoadOption,来指示
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 sql对DataLoadOptions的使用是有限制的,它只支持1个1对多的关系。一个顾客可能有多个订单,一个订单可能有多个详细订单:
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的关系,所以也只会有1次SQL(不过这样的操作还是少执行为妙,消耗太大了):
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 sql在TopicType为0的时候识别为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的时候类型就是NewTopic,1的时候就是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(); |
newtopic和replies是两个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>();
}
} |
CategoryID和CategoryName的映射没有什么不同,只是我们还增加了一个Boards属性,它返回的是Board实体集。通过特性,我们定义了关系外键为BoardCategory(Board表的一个字段)。然后来看看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引用B和D;
-
B引用D和System.ServiceModel程序集
- C引用B、D、System.ServiceModel以及System.Data.Linq程序集
-
D引用System.Data.Linq程序集
-
E引用C和System.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到这里就结束了,看到这里应该已经算师父领进门了,后续的提高还要靠大家自己去琢磨。
|