DLinq 2006年Preview引入了很多新特性。如继承、连接、多层程序设计支持等新特性,这些特性我们在前面的陈述过了,还有一些特性如外部映射和显著增强的存储过程支持,则直接基于用户对Preview的反馈。
DLinq支持单一表格的映射,也就是说,实体类以及它的子类的实体数据都被存储在同一个表格中。整个继承层次的所有数据都被扁平地存放在同一个数据库表格中,如果某行所对应的某种类型的实体不包含某个指定列,则将此列设为空值。单一表格映射存储是一种最简单的表达继承层次的方式,这种方式为多样性查询提供了优良的性能特性。
使用DLinq实现继承层次的映射,您需要为基实体类指定下面的特性以及特性属性:
而在子类中不需要特别指定特性或者属性。请注意,子类不需要指定[Table]特性。
在下面的列子中,Vehicle 的子类Car和Truck中的数据被映射到了同一个数据库表格Vehicle中。(为了简单化这个例子,示例代码中,用成员而不是属性来指定列的映射关系。)
[Table]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
IsDefault = true)]
public class Vehicle
{
[Column(IsDiscriminator = true)]
public string DiscKey;
[Column(Id = true)]
public string VIN;
[Column]
public string MfgPlant;
}
public class Car : Vehicle
{
[Column]
public int TrimCode;
[Column]
public string ModelName;
}
public class Truck : Vehicle
{
[Column]
public int Tonnage;
[Column]
public int Axles;
}
类图如下:
在服务器浏览器(Server Explorer)中查看我们得到的数据库视图,您可以看到所有的列都映射到了同一个表格中,如下:
下面的代码片段告诉您如何在您的查询中尝试使用子类:
var q = db.Vehicle.Where(p => p is Truck);
//or
var q = db.Vehicle.OfType<Truck>();
//or
var q = db.Vehicle.Select(p => p as Truck).Where(p => p != null);
foreach (Truck p in q)
Console.WriteLine(p.Axles);
您可以基于上面简单的示例扩展继承关系。
例1
下面演示了更丰富的继承层次和更加复杂的查询:
[Table]
[InheritanceMapping(Key = "V", Type = typeof(Vehicle), UseAsDefault = true)]
[InheritanceMapping(Key = "C", Type = typeof(Car))]
[InheritanceMapping(Key = "T", Type = typeof(Truck))]
[InheritanceMapping(Key = "S", Type = typeof(Semi))]
[InheritanceMapping(Key = "D", Type = typeof(DumpTruck))]
public class Truck: Vehicle { ... }
public class Semi: Truck { ... }
public class DumpTruck: Truck { ... }
...
// Get all trucks along with a flag indicating industrial application.
db.Vehicles.OfType<Truck>.Select(t =>
new {Truck=t, IsIndustrial=t is Semi || t is DumpTruck }
);
例2
下面的继承层次包括一下接口:
[Table]
[InheritanceMapping(Key = "V", Type = typeof(Vehicle),
UseAsDefault = true)]
[InheritanceMapping(Key = "C", Type = typeof(Car))]
[InheritanceMapping(Key = "T", Type = typeof(Truck))]
[InheritanceMapping(Key = "S", Type = typeof(Semi))]
[InheritanceMapping(Key = "H", Type = typeof(Helicopter))]
public class Truck: Vehicle
public class Semi: Truck, IRentableVehicle
public class Helicopter: Vehicle, IRentableVehicle
可能的查询如下:
// Get commercial vehicles ordered by cost to rent.
db.Vehicles.OfType<IRentableVehicle>.OrderBy(cv => cv.RentalRate);
// Get all non-rentable vehicles
db.Vehicles.Where(v => !(v is IRentableVehicle));
在两层应用程序中,一个DataContext可以处理所有的查询和更新。尽管如此,而多层应用程序则有所不同,它需要使用多个独立的DataContext实例来执行查询和更新。例如,对于一个ASP.NET应用,web服务器需要通过处理多个独立的查询和更新。因此,使用同一个DataContext实例来同时执行多个请求是很不实际的。在这种情况下,DataContext实例需要能够更新不是从数据库中返回的实体。DLinq通过提供Attach()方法来提供对多层实体的支持。
下面的例子告诉我们怎样使用不同的DataContext实例来更新Customer对象:
Northwind db1 = new Northwind(…);
Customer c1 = db1.Customers.Single(c => c.CustomerID == "ALFKI");
// Customer entity changed on another tier - e.g. through a browser
// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);
// Create a new entity for applying changes
Customer C2 = new Customer();
C2.CustomerID = originalID;
// Set other properties needed for optimistic concurrency check
C2.CompanyName = originalCompanyName;
...
// Tell DLinq to track this object for an update; i.e. not for insertion
db2.Customers.Attach(c2);
// Now apply the changes
C2.ContactName = "Mary Anders";
// DataContext now knows how to update the customer
db2.SubmitChanges();
在多层应用程序中,很少有实体因为简单性、互操作性和保密性而被跨层使用。例如,一个web服务提供者可能会根据中间层Order实体类来定义不同于Order的一个数据契约(Data Contract)来实现网络上的数据传输。同样地,显示层中的网页也只是显示Employee实体的部分成员数据。因而,DLinq对多层实体的支持就是为了适应这些情况。只有属于下面的一类或者多类实体需要跨层传输,在修改数据之前需要调用Attach()方法:
如果时间戳列或者版本号列用于执行乐观并发检查,则相应的实体成员被设置之前需要调用Attach()方法。而其他成员则不需要这样做。DLinq执行最小化带乐观并发检查的更新。也就是说,没有发生改变的或不参与乐观并发检查的成员在更新的时候都将被忽略。
如果需要保留参与乐观并发检查的原始值则需要DLinq之外的其他机制来实现。一个ASP.NET应用可以用视状态(View State)(或者一个使用View State的空控件)来实现这一点。一个web服务可以使用DataContract在进行数据更新的时候使初始值可用。而在互操作性和一般性的问题上,DLinq并没有指定不同层之间的数据交换格式和用于来回传输初始值的机制。
实体的插入和删除不要调用Attach()方法。Table.Add() 和Table.Remove()可用于两层应用中的插入和删除。对于两层应用的数据更新,一个用户负责处理外键约束。拥有多个Orders的Customer不能简单地删除某个Order,而不处理外键约束,因为数据库会组织您删除某个Order。
DLinq在更新实体时也能处理实体的Attach。用户需要创建一个需要更新的对象,然后调用Attach()方法。为了完成必要的更新,实体的所有变化在Attach视图中可以被“重现”,如下所示:
Northwind db1 = new Northwind(…);
// Assume Customer c1 and related Orders o1, o2 are retrieved
// Back on the mid-tier, a new context needs to be used
Northwind db2 = new Northwind(…);
// Create new entities for applying changes
Customer c2 = new Customer();
c2.CustomerID = c.CustomerID;
Order o2 = new Order();
o2.OrderID = ...;
c2.Orders.Add(o2);
// Add other related objects needed for updates
// Set properties needed for optimistic concurrency check
...
// Order o1 to be deleted
Order o1 = new Order();
o1.OrderID = ...;
// Tell DLinq to track the graph transitively
db2.Customers.Attach(c2);
// Now "replay" all the changes
// Updates
c2.ContactName = ...;
o2.ShipAddress = ...;
// New object for insertion
Order o3 = new Order();
o3.OrderID = ...;
c2.Orders.Add(o3);
// Remove order o1
db2.Orders.Remove(o1);
// DataContext now knows how to do update/insert/delete
db2.SubmitChanges();
EntitySet现在支持下面这种方式的远程查询。请看下面的例子:
Customer c = db.Customers.Single(x => x.CustomerID == …);
foreach (Order ord in
c.Orders.Where(o => o.ShippedDate.Value.Year == 1998)) {
...
}
我们假设您有成千上万个Order,您只是想处理其中的一部分Order,因此您并不想获取到所有的Order。上面例子中的EntitySet通过本地查询加载了所有的Orders并通过Where()操作符筛选您所需要的数据。现在的EntitySet实现了IQueryable<T>,而且保证了上面的查询可以被远程执行。优点有二:不获取不必要的数据;通过数据库索引来实现远程查询,性能更优。
在其他情况下,您可能想要获取到所有关联的实体。新增加的EntitySet的Load()方法可以显示地加载EntitySet的所有成员。如果EntitySet已经被加载了,后续的查询将会在本地执行。优点有二:如果所有的实体都需要多次在本地被使用,可以省去不需要的远程查询和相关的延迟;而且实体可以被完整地序列化。下面的代码片段告诉您怎样实现本地查询:
Customer c = db.Customers.Single(x => x.CustomerID == …);
c.Orders.Load();
// 因此,c.Orders 是实现了IEnumerable<T> 的本地集合
foreach (Order ord in
c.Orders.Where(o => o.ShippedDate.Value.Year == 1998)) {
...
}
上面提到的两种方式的结合提供了强大的处理能力-对大量数据的远程查询和对少量数据或根据需要对大量数据的本地查询。远程获取到的是IQueryable<T>,而相反的是,本地查询获取到的内存集合IEnumerable<T>。
实现了IList<T>的本地集合对象和通过远程查询从关系数据库中的获取到无序集合是有很大的区别的。IList<T>根据索引来获取值的某些方法是列表所特有的,而通常来说,通过远程查询获取到的无序集合是不能做到这点的。因此,这些方法会隐式地把远程查询到的结果加载到EntitySet中,从而允许本地查询。事实上,在上个版本中,这些方法的调用也常常会引起EntitySet的加载。
本地和远程查询行为的另外一个关键区别是, EntitySet可以允许执行多个远程查询,除非采用强制加载。远程查询反映的是数据库中的数据,而非本地集合。因为并行查询可能存在于数据库中,而两次查询之间数据库的状态已经发生了变化,因此同一个查询分两次执行可能会导致不同的结果。最终的结果反映的是数据库中插入/删除而引起的数据改变,而在本地通过Add() 和 Remove()调用并不会引起数据库状态的改变。
从本质上来说,对一个还未被加载的EntitySet的查询来说它的数据还存在远程的数据库上,而对于已经记载到内存中的EntitySet来说,它在本地是实际存在的。隐式或者显式地调用Load(),也都符合上面的规则。
大多数对于实体模型的查询很大程度上依赖于实体模型中实体彼此之间的引用关系。尽管如此,在实际的实体模型中,很多有趣的实体之间的“关联”并不依赖于实体之间的引用。例如,Customer.Orders就是在Northwind数据库中基于外键相关联的。而同一个城市中通过局域网络互联的Suppliers和Customers并不是基于外键相关联的,因此通过查询得到的实体模型它们并没有任何的引用关系。连接是用来处理这种关系的一个附加的查询机制。DLinq支持在LINQ中最新引入的连接操作。
请思考下面的例子的问题-查询在同一个城市的所有的supplier和 customer。这个查询返回了同一个城市中所有的supplier和customer的公司的名称。这样的查询方式等同于数据库查询中的内连接。
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City
select new {
Supplier = s.CompanyName,
Customer = c.CompanyName,
City = c.City
};
上面的查询丢弃了不在同一个城市的supplier和customer。但是有些时候,我们还是想得到关系网络中的这些数据。在下面查询中,我们列出了所有的supplier以及每个supplier的所拥有的customer。如果某个supplier在同一个城市中没有customer,那么返回的就是customer的一个空集合。请注意我们得到的并不是一个扁平的数据结构,每个supplier都对应一个customer的集合。这就是group join,它有效地连接了两个序列,并把第二个序列查询的结果以集合的形式作为第一个序列得到的结果的元素。
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
select new { s, scusts };
Group join也可扩展到多个集合的查询。下面的查询就扩展了上面的查询,列出所有的在同一个城市的supplier以及employee。在这个查询里面,一个supplier包含有一个customer和employee的集合(集合可能为空)。
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
join e in db.Employees on s.City equals e.City into semps
select new { s, scusts, semps };
一个group join查询得到的结果也可能是扁平的。对Suppliers和customers的连接查询有多个入口,我们可以让同一个城市中的每个supplier对应一个customer。空集合用空值取代。这就等同于关系数据的等外连接。
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into sc
from x in sc.DefaultIfEmpty()
select new {
Supplier = s.CompanyName,
Customer = x.CompanyName,
City = x.City
};
基本的连接查询操作符都在Standard Query Operators中有介绍。DLinq只支持等连接,而且要求等连接的判断条件的两端必须是相同类型的。
DLinq除了支持基于特性的映射方式,还支持外部映射的方式。外部映射通常的方式就是采用XML。外部文件映射的方式提供了另外一种解决方案,它可以将代码和映射相分离,这是一种很好的方式。
DataContext的构造函数中允许传入一个MappingSource对象。我们可以使用XML映射文件创建XmlMappingSource对象,XmlMappingSource是MappingSource其中的一种。下面是采用XML文件映射的例子:
String path = @"C:/Mapping/NorthwindMapping.xml";
XmlMappingSource prodMapping =
XmlMappingSource.FromXml(File.ReadAllText(path));
Northwind db = new Northwind(
@"Server=./SQLExpress;Database=c:/Northwind/Northwnd.mdf",
prodMapping
);
下面显示的是Product类的XML映射的相应的代码片段。代码显示,Mapping.Product映射到Northwind数据库中的Products标。这里面的XML元素和XML属性与实体类的特性的名称和参数都是一致的。
<Database Name="Northwind">
<Table Name="Products">
<Type Name="Mapping.Product">
<Column Name="ProductID" Member="ProductID" Storage="_ProductID"
DbType="Int NOT NULL IDENTITY" IsIdentity="True"
IsAutoGen="True" />
<Column Name="ProductName" Member="ProductName"
Storage="_ProductName" DbType="NVarChar(40) NOT NULL" />
<Column Name="SupplierID" Member="SupplierID" Storage="_SupplierID"
DbType="Int" />
<Column Name="CategoryID" Member="CategoryID" Storage="_CategoryID"
DbType="Int" />
<Column Name="QuantityPerUnit" Member="QuantityPerUnit"
Storage="_QuantityPerUnit" DbType="NVarChar(20)" />
<Column Name="UnitPrice" Member="UnitPrice" Storage="_UnitPrice"
DbType="Money" />
<Column Name="UnitsInStock" Member="UnitsInStock"
Storage="_UnitsInStock" DbType="SmallInt" />
<Column Name="UnitsOnOrder" Member="UnitsOnOrder"
Storage="_UnitsOnOrder" DbType="SmallInt" />
<Column Name="ReorderLevel" Member="ReorderLevel"
Storage="_ReorderLevel" DbType="SmallInt" />
<Column Name="Discontinued" Member="Discontinued"
Storage="_Discontinued" DbType="Bit NOT NULL" />
<Association Name="FK_Order_Details_Products" Member="OrderDetails"
Storage="_OrderDetails" ThisKey="ProductID"
OtherTable="Order Details" OtherKey="ProductID" />
<Association Name="FK_Products_Categories" Member="Category"
Storage="_Category" ThisKey="CategoryID" OtherTable="Categories"
OtherKey="CategoryID" IsParent="True" />
<Association Name="FK_Products_Suppliers" Member="Supplier"
Storage="_Supplier" ThisKey="SupplierID" OtherTable="Suppliers"
OtherKey="SupplierID" IsParent="True" />
</Type>
</Table>
<Table Name="Order Details">
<Type Name="Mapping.OrderDetail">
</Type>
</Table>
...
</Database>
为了简单化,我们省略了其他表格/类和存储过程/函数元素。Preview的MSI中包含Northwind数据库的样例XML映射文件以及相应的样例代码。其中的Schema文件是“DLinq Mapping Schema.xsd”。就目前来讲,外部映射和采用编码的形式进行映射所要设置的参数和5.1和6.4节所提到的内容是一致的。
DLinq支持存储过程和用户自定义函数。DLinq把存储过程和用户自定义函数映射成本地的过程和方法,所以您可以通过以一种强类型的方式访问客户端代码。您可以很容易地使用IntelliSense™找出这些方法,而且这些方法的签名类似数据库中定义的存储过程和方法的签名。通过调用这个生成的方法,您可以得到一个结果集,它是一个强类型集合。DLinq可以帮助您自动生成这些方法,但您也可以选择不使用自动代码生成而手动编码这些方法。
DLinq通过使用本地方法的参数和属性来映射存储过程和方法。StoredProcedure,Parameter和Function特性都有一个名称属性,Parameter特性还有一个DBType属性。下面是两个例子:
[StoredProcedure(Name="CustOrderHist")]
public StoredProcedureResult<CustOrderHistResult>
CustOrderHist([Parameter(Name="CustomerID")] string customerID)
{
return this.ExecuteStoredProcedure<CustOrderHistResult>
(((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
}
[Function(Name="[dbo].[ConvertTemp]")]
public string ConvertTemp(string string) { ... }
下面这些例子告诉我们怎样映射不同类型的存储过程。
例1
下面的存储过程有一个输入参数并返回一个整数。
CREATE PROCEDURE GetCustomerOrderCount(@CustomerID nchar(5))
AS
Declare @count int
SELECT @count = COUNT(*) FROM ORDERS WHERE CustomerID = @CustomerID
RETURN @count
上面的存储过程映射到本地的方法如下:
[StoredProcedure(Name="GetCustomerOrderCount")]
public int GetCustomerOrderCount(
[Parameter(Name="CustomerID")] string customerID
)
{
StoredProcedureResult result = this.ExecuteStoredProcedure(
((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
return result.ReturnValue.Value;
}
例2
当一个存储过程需要返回多个结果,那么我们不能返回单个强类型。在下面的例子中,返回结果的形式依赖于输入参数:
CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
select OrderID, ShipName from orders
映射方法如下:
[StoredProcedure(Name="VariableResultShapes")]
public StoredProcedureMultipleResult VariableResultShapes(
System.Nullable<int> shape)
{
return ((StoredProcedureMultipleResult)(
this.ExecuteStoredProcedure(
((MethodInfo)(MethodInfo.GetCurrentMethod())), shape)
)
);
}
您可以按照下面的方法使用这个存储过程:
StoredProcedureMultipleResult result =
db.VariableResultShapes(1);
foreach (VariableResultShapesResult1 c in
result.GetResults<VariableResultShapesResult1>()) {
Console.WriteLine(c.CompanyName);
}
result = db.VariableResultShapes(2);
foreach (VariableResultShapesResult2 o in
result.GetResults<VariableResultShapesResult2>()) {
Console.WriteLine(o.OrderID);
}
在这里,您可以根据您对存储过程的了解使用GetResults方法返回一个正确类型的迭代器。DLinq会尽可能得到一致的结果,但却无法保证返回顺序的一致。如果您想知道映射方法返回的结果类型,唯一的方式就是查看映射方法的注释。
例 3
下面的存储过程的T-SQL返回的多个排序的结果:
CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers
这个存储过程的映射方法和上面的例2一致。但是这个例子产生的是两个顺序的结果集:
[StoredProcedure(Name="MultipleResultTypesSequentially")]
public StoredProcedureMultipleResult MultipleResultTypesSequentially()
{
return ((StoredProcedureMultipleResult)(
this.ExecuteStoredProcedure(
((MethodInfo)(MethodInfo.GetCurrentMethod())))
)
);
}
您可以按照下面的方式使用存储过程:
StoredProcedureMultipleResult sprocResults =
db.MultipleResultTypesSequentially();
//first read products
foreach(Product p in sprocResults.GetResults<Product>()) {
Console.WriteLine(p.ProductID);
}
//next read customers
foreach (Customer c in sprocResults.GetResults<Customer>()) {
Console.WriteLine(c.CustomerID);
}
例 4
DLinq将存储过程的out参数映射成本地方法的引用参数(ref关键字)。如果out参数类型为值类型,这个参数可以为空。下面的存储过程带有一个输入参数和一个输出参数:
CREATE PROCEDURE GetCustomerCompanyName(
@customerID nchar(5),
@companyName nvarchar(40) output
)
AS
SELECT @companyName = CompanyName FROM Customers
WHERE CustomerID=@CustomerID
映射方法如下:
[StoredProcedure(Name="GetCustomerCompanyName")]
public int GetCustomerCompanyName(string customerID, ref string companyName)
{
StoredProcedureResult result =
this.ExecuteStoredProcedure(
((MethodInfo)(MethodInfo.GetCurrentMethod())),
customerID,
companyName
);
companyName = ((string)(result.GetParameterValue(1)));
return result.ReturnValue.Value;
}
在这个例子里面,方法并没有显示返回一个值,但会返回一个默认值。对于输出参数,如您期望的那样映射方法也有一个输出参数。
您可以按照下面的方式调用上面的存储过程,如下:
string CompanyName = "";
string customerID = "ALFKI";
db.GetCustomerCompanyName(customerID, ref CompanyName);
Console.WriteLine(CompanyName);
例 5
在这个例子里面,存储过程的返回类型(StoredProcedureResult<GetCustomerOrders_Result>)一个强类型的,它实现了IEnumerable<GetCustomerOrders_Result>。强类型GetCustomerOrders_Result是代码工具自动生成的特定的数据类型,它包含了结果集中的所有列所对应的属性。在函数返回之前,结果被强制地缓冲,直到您调用GetParameterValue。
CREATE PROCEDURE GetCustomerOrdersAndCount(
@customerID nchar(5),
@count int OUTPUT
)
AS
select OrderID, ShipName, OrderDate, Freight from orders
where customerid = @customerID
SELECT @count=@@ROWCOUNT
映射方法如下:
[StoredProcedure(Name="GetCustomerOrdersAndCount")]
public StoredProcedureResult<GetCustomerOrdersAndCountResult>
GetCustomerOrdersAndCount(
string customerID,
ref System.Nullable<int> count
)
{
StoredProcedureResult<GetCustomerOrdersAndCountResult> result =
this.ExecuteStoredProcedure<GetCustomerOrdersAndCountResult>(
((MethodInfo)(MethodInfo.GetCurrentMethod())),
customerID,
count
);
count = ((System.Nullable<int>)(result.GetParameterValue(1)));
return result;
}
DLinq支持返回值为标量和集合的函数,并且可以将这些函数以内联的方式实现。DLinq内联标量调用和系统预定义函数的调用方式基本一致。思考下面的查询:
var q =
from p in db.Products
select
new {
pid = p.ProductID,
unitp = Math.Floor(p.UnitPrice.Value)
};
在这里,Math.Floor被翻译成系统方法‘FLOOR’。同样的,映射到UDF的方法调用也会被翻译成SQL中的UDF。
例 1
下面是一个标量用户自定义函数(UDF)ReverseCustName()。在SQL服务器中,这个函数的定义如下:
CREATE FUNCTION ReverseCustName(@string varchar(100))
RETURNS varchar(100)
AS
BEGIN
DECLARE @custName varchar(100)
// Impl. left as exercise for the reader
RETURN @custName
END
您可以在的类中加入映射上面的UDF的方法定义,如下。可以看到,方法的主体构建了一个表达式来表达函数调用的目的,并且把DataContext传递给这个表达式用于翻译和执行(如果您在一个查询之外调用这个方法,UDF将被执行)。
[Function(Name="[dbo].[ReverseCustName]")]
public string ReverseCustName(string string)
{
MethodCallExpression mc =
Expression.Call(
((MethodInfo)(MethodInfo.GetCurrentMethod())),
Expression.Constant(this),
new Expression[] {
Expression.Constant(@string, typeof(string))
}
);
return Sequence.Single(this.CreateQuery<string>(mc));
}
例 2
在下面的查询中,您可以看到我们调用了生成的UDF方法ReverseCustName。在这个例子中,UDF方法并不会马上被执行。这个查询里面的UDF方法将会把翻译成相应的SQL语句(查询的后面是相应的SQL语句)。
var q =
from c in db.Customers
select
new {
c.ContactName,
Title = db.ReverseCustName(c.ContactTitle)
};
è
SELECT [t0].[ContactName],
dbo.ReverseCustName([t0].[ContactTitle]) AS [Title]
FROM [Customers] AS [t0]
当您在查询之外调用这个方法的时候,DLinq会根据UDF方法表达式来创建一个简单的SQL查询,SQL语法如下(这里面的参数@p0绑定为传入的常参)。
SELECT dbo.ReverseCustName(@p0)
DLinq调用:
string str = db.ReverseCustName("DLinq");
例 3
表格函数(TVF) 返回一个行集(不同于存储过程的是,它可以返回多个结果类型)。因为TVF返回的是一个表格,所以您可以在使用表格的SQL语句中任意使用TVF,您可以把TVF当成一般的数据库表格来使用。
思考下面的SQL Server中定义的TVF。
CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
SELECT ProductID, UnitPrice
FROM Products
WHERE UnitPrice > @cost
这个函数显示指定返回一个TABLE,所以返回的行集的结构也就被隐式地定义了。DLinq会把这个TVF映射为下面的方法:
[Function(Name="[dbo].[ProductsCostingMoreThan]")]
public IQueryable<ProductsCostingMoreThanResult>
ProductsCostingMoreThan(System.Nullable<decimal> cost)
{
MethodCallExpression mc =
Expression.Call(
((MethodInfo)(MethodInfo.GetCurrentMethod())),
Expression.Constant(this),
new Expression[] {
Expression.Constant(cost, typeof(System.Nullable<decimal>))
}
);
return this.CreateQuery<ProductsCostingMoreThanResult>(mc);
}
您可以利用TVF返回的表格用于下面的SQL查询,就像使用其他普通的表格一样。
SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID
DLinq查询将会使用下面的语法(使用了“join”语法):
var q =
from p in db.ProductsCostingMoreThan(80.50m)
join s in db.Products on p.ProductID equals s.ProductID
select new {p.ProductID, s.UnitPrice};
DLinq能够对静态地返回结果集合的一般的存储过程进行映射。但是DLinq并不支持下面类型的存储过程:
• 使用动态的SQL返回结果集的存储过程。如果一个存储过程根据不同地条件来执行不同的SQL语句,因为这个查询得到的结果要直到运行的时候才能知道,所以DLinq不能据此就得到结果集的元数据。
• 直接使用临时表的存储过程。
当客户端试图提交上一次读取操作获取的实体的变化时,而且有一个或者多个值需要进行更新检查,这个时候就可能会发生乐观式并发冲突。(注意:只有指定有特性UpdateCheck.Always 或UpdateCheck.WhenChanged的成员才会参与乐观并发检查。如果一个成员被指定为UpdateCheck.Never,则它不参与并发检查。)
并发冲突处理包括发现实体的哪些成员在提交的时候发生了冲突,然后决定实行什么样的措施。
所谓的冲突处理就是通过重新查询数据库或消除一致性差异来更新冲突的数据的过程。当需要更新某个实体的时候,变化跟踪处理器会保留实体的原始值和新值。DLinq然后来决定实体在更新的时候是否发生了冲突。如果是,DLinq首先会找出是那些成员的更新出现了冲突。如果要更新的某个成员的值不同于原始值(这个值用于更新检查),这就意味着发生了并发冲突。如果某个成员发生了冲突,它将被添加到一个冲突列表中。
例如,在下面的表格中,User1通过查询数据库中的某行来开始准备更新数据。而在User1提交更新的时候User2改变了数据库的数据。User1提交失败,因为Col B 和 Col C的数据已经发生了改变。
|
Col A |
Col B |
Col C |
original state |
Alfreds |
Maria |
Sales |
User1 |
Alfred |
|
Marketing |
User2 |
|
Mary |
Service |
在DLinq中,使用乐观并发冲突发生如果实体更新失败,那么DLinq会抛出一个异常(OptimisticConcurrencyException)。您可以设置第一次更新失败的时候是否需要抛出异常或对异常包含的失败试图进行恢复。
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
db.SubmitChanges(ConflictMode.ContinueOnConflict);
当异常被抛出的时候,您可以通过抛出的异常访问OptimisticConcurrencyConflict集合。每个冲突(对应一个失败更新尝试)都会有详细的描述,包括访问OptimisticConcurrencyMemberConflict列表的方法。在冲突检测中更新失败的成员都有一个映射的冲突。
在前面的章节中,User1的RefreshMode选项用于设置在试图重新提交的时候如何消除数据不一致,这些选项如下。在所有的例子中,客户的记录需要从数据库中读取数据来刷新。这可以保证下一次更新尝试不会因为同一次冲突检测而失败。
KeepChanges
使用这个选项,User1会合并客户端和数据库中的数据,所以只有此次变更中改变的数据才会覆盖数据库中的数据(请参考这节中的例1)。
采用这种方式,下面是冲突处理的数据库中的最终结果:
|
Col A |
Col B |
Col C |
KeepChanges |
Alfred (User1) |
Mary (User2) |
Marketing (User1) |
Col A: 写入User1变化了的数据(Alfred)
Col B: 保留User2 的数据(因为User1中的这个数据没有改变,所以保留此数据)。
Col C: 写入User1变化了的数据(Marketing)。因为User1的这个数据发生了变化,所以User2的数据(Service) 被覆盖
KeepCurrentValues
使用这个选项,User1将覆盖数据库中相应的数据(请参考这节中后面的例2)。
User1提交更新之后。数据库中的结果如下:
|
Col A |
Col B |
Col C |
KeepCurrentValues |
Alfred (User1) |
Maria (Original) |
Marketing (User1) |
Col A: 写入User1变化了的数据(Alfred)
Col B: 保留User1的数据Maria,丢弃 User2的数据
Col C: 写入User1变化了的数据(Marketing),丢弃 User2的数据(Service) 。
OverwriteCurrentValues
使用这个选项,User1数据将会使用数据库中的数据进行填充(参考这节后面的例3)。
User1提交更新之后。数据库中的结果如下:
|
Col A |
Col B |
Col C |
OverwriteCurrentValues |
Alfreds (Original) |
Mary (User2) |
Service (User2) |
Col A: 保留User1 的旧值(Alfreds),丢弃User1 的新值(Alfred)
Col B: User2的值
Col C: User2的值(Service),丢弃 User1的新值 (Marketing) 。
处理完这些冲突之后您就可以试图再次提交数据。因为第二次更新也可能失败,所以需要试图循环地进行数据更新。
在DLinq中,支持OCCR的类和特征如下:
更多内容,请打开Visual Studio对象浏览器。
下面的代码片段提供了帮助您进行冲突检测和处理的相关信息和技术。
例1
在这个例子里面,冲突将被“自动”处理。也就是说,只有客户端变化的值才会被更新到数据库(KeepChanges)。当个别成员冲突出现时,需要自行处理。
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
//自动合入变化的当前值到数据库中
e.Resolve(RefreshMode.KeepChanges);
}
//第二次尝试中提交成功
context.SubmitChanges(ConflictMode.FailOnFirstConflict);
例2
在这个例子中,冲突也可以被自动处理,不需要客户端进行处理,但是数据库的值不会并入当前客户端的值。
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
foreach (OptimisticConcurrencyConflict cc in e.Conflicts) {
//数据库的值不并入当前值
cc.Resolve(RefreshMode.KeepCurrentValues);
}
}
例3
在这个例子中,冲突也可以被自动处理,不需要客户端进行处理,但是客户端的数据将使用数据库当前的值进行更新。
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
foreach (OptimisticConcurrencyConflict cc in e.Conflicts) {
//当前值被数据库中的值更新
cc.Resolve(RefreshMode.OverwriteCurrentValues);
}
}
例4
在这个例子中,告诉我们在实体提交发生冲突的时候如何访问必要的信息。
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (OptimisticConcurrencyConflict cc in e.Conflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
}
}
例5
在这个例子中,我们循环地处理每个冲突。在这里,您可以提供自定义的处理方式(注:获取MemberInfo需要添加using System.Reflection)。
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (OptimisticConcurrencyConflict cc in e.Conflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
foreach (OptimisticConcurrencyMemberConflict mc in
cc.GetMemberConflicts()) {
object currVal = mc.CurrentValue;
object origVal = mc.OriginalValue;
object databaseVal = mc.DatabaseValue;
MemberInfo mi = mc.MemberInfo;
Console.WriteLine("Member: {0}", mi.Name);
Console.WriteLine("current value: {0}", currVal);
Console.WriteLine("original value: {0}", origVal);
Console.WriteLine("database value: {0}", databaseVal);
Console.ReadLine();
}
}
}
例 6
这个例子是一个试验,用于处理成员冲突。这段代码模拟了两个不同的user的提交。User1先提交更新,但是在User1提交更新的时候,User2已经改变了数据中的数据。
您可以修改下面的代码段,改变RefreshMode选项(KeepChanges, KeepCurrentValues,或OverwriteCurrentValues),运行下面的代码,查看数据库的变化。发生冲突的两个成员(Col B 和Col C)将会被写到Console中。
在这个例子中,为了简单起见,只有一行数据被更新,但是ConflictMode当设置为ContinueOnConflict,我们添加了一个循环来处理列表中的成员冲突。
同例5,您需要添加System.Reflection来获取MemberInfo。
static void Main(string[] args) {
//重置表格数据为原始值
Northwnd resetdb = new Northwnd(
@"C:/program files/linq preview/data/northwnd.mdf");
var q = resetdb.Customers.Single(c => c.CustomerID == "ALFKI");
q.CompanyName = "Alfreds Futterkiste";
q.ContactName = "Maria Anders";
q.ContactTitle = "Sales Representative";
resetdb.SubmitChanges();
//User1在开始更新之前查询数据库
Northwnd user1 = new Northwnd(resetdb.Connection);
var q1 = user1.Customers.Single(c => c.CustomerID == "ALFKI")
//User2在User1提交变化之前改变数据
Northwnd user2 = new Northwnd(resetdb.Connection);
var q2 = user2.Customers.Single(c => c.CustomerID == "ALFKI")
q2.ContactName = "Mary Anders";
q2.ContactTitle = "Service Representative";
user2.SubmitChanges();
//User1尝试提交变化
q1.CompanyName = "Alfred Futterkiste";
q1.ContactTitle = "Marketing Representative";
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (OptimisticConcurrencyException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (OptimisticConcurrencyConflict cc in e.Conflicts) {
//改变RefreshMode选项
cc.Resolve(RefreshMode.KeepChanges);
Customers entityInConflict = (Customers)cc.Object;
ITable table = cc.Table;
//OptimisticConcurrencyMemberConflict 信息
foreach (OptimisticConcurrencyMemberConflict mc in
cc.GetMemberConflicts()) {
object currVal = mc.CurrentValue;
object origVal = mc.OriginalValue;
object databaseVal = mc.DatabaseValue;
MemberInfo mi = mc.MemberInfo;
Console.WriteLine("Member: {0}", mi.Name);
Console.WriteLine("current value: {0}", currVal);
Console.WriteLine("original value: {0}", origVal);
Console.WriteLine("database value: {0}", databaseVal);
Console.ReadLine();
//手动合并,保留客户端变化
if ( !mc.HaveModified)
//使用任何一个值或选择一个RefreshMode 选项来处理冲突
mc.Resolve(databaseVal);
}
}
}
//User1试图再次更新
user1.SubmitChanges();
Console.WriteLine("Changes submitted");
Console.ReadLine();
}
在下面几节中,我们列出了DLinq支持的.NET Framework类型和一些差异。
已经实现的
算术和比较操作符
移位操作符:<< 和>>
使用SQL’s POWER的转换操作Cast, Object.ToString:
数字转换为SQL中的字符UNICODE / NCHAR,否则使用的SQL的 CONVERT方法
没有实现的
<Type>.Parse
枚举类型映射为表格中的整型,但是DLinq不支持整型值和枚举值之间的转换以及枚举值和字符串逐渐的转换(Parse / ToString)。
与.NET 的区别
双精度数的ToString将使用SQL中的CONVERT(NVARCHAR(30), @x, 2)进行转换,而SQL通常使用16位科学计算,如0的值为“0.000000000000000e+000”。所以,这和.NET的Convert.ToString的结果是有差别的。
已经实现的
非静态方法:
Length, Substring, Contains, StartsWith, EndsWith, IndexOf, Insert, Remove, Replace, Trim, ToLower, ToUpper, LastIndexOf, PadRight, PadLeft, Equals, CompareTo.
支持的方法签名,但不支持带StringComparison参数的任何方法,详细描述如下:
静态方法:
string Concat(...); //all signatures
int Compare(string strA, string strB);
char String[int]
static String.Equals(string a, string b)
构造函数:
String(char, int)
重载操作符:
+, ==, !=
没有实现的
IFormatProvider 使用或者返回字符串数组以及使用CultureInfo / StringComparison / IFormatProvider的函数:
静态方法:
string Copy(string str)
int Compare(string strA, string strB, bool ignoreCase);
int Compare(string strA, string strB, StringComparison comparisonType);
int Compare(string strA, string strB, bool ignoreCase, CultureInfo culture);
int Compare(string strA, int indexA, string strB, int indexB, int length);
int Compare(string strA, int indexA, string strB, int indexB, int length, bool ignoreCase);
int Compare(string strA, int indexA, string strB, int indexB, int length, StringComparison comparisonType);
int Compare(string strA, int indexA, string strB, int indexB, int length, bool ignoreCase, CultureInfo culture);
int CompareOrdinal(string strA, string strB);
int CompareOrdinal(string strA, int indexA, string strB, int indexB, int length);
string Join(string separator, string[] value [,...])
非静态方法:
string ToUpperInvariant()
string Format(string format, object arg0) + overloads
int IndexOf(string value, int startIndex, StringComparison comparisonType)
int IndexOfAny(char[] anyOf)
string Normalize(), bool IsNormalized()
string Normalize(NormalizationForm normalizationForm)
string[] Split(...)
bool StartsWith(string value, StringComparison comparisonType)
char[] ToCharArray()
string ToUpper(CultureInfo culture)
string TrimEnd(params char[] trimChars)
string TrimStart(params char[] trimChars)
同.NET的区别/限制
SQL使用collations对字符串进行排序和比较。可以在SQL Serve实例, 数据库, 表格列,或者一个表达式中指定。
目前实现的函数的转换没有改变collation或者在翻译的表达式中指定不同的collation。所以,如果默认的collation是大小写敏感的, CompareTo 或 IndexOf返回的结果将不同于.NET相应方法(也是大小写敏感)。
StartsWith(str) / EndsWith(str)方法假设参数str是一个常量或者是一个在客户端计算的表达式。也就是说,目前str不可能为某列的值。
已经实现的静态方法
Abs, Acos, Asin, Atan, Atan2, BigMul, Ceiling, Cos, Cosh, Exp, Floor, Log, Log10, Max, Min, Pow, Sign, Sinh, Sqrt, Tan, Tanh, Truncate --all signatures
没有实现的
IEEERemainder
DivRem有一个out参数,所以在一个表达式中使用它。因为Math.PI 和 Math.E在客户端中被计算出来了,所以不需要做任何转换。
与.NET的区别
.NET中的Math.Round被翻译成SQL函数ROUND。尽管如此,SQL中的ROUND和.NET的Math.Round有点微小的差别。如果最后一个数字是5,在取值的时候Math.Round取最接近的偶数值(例如,2.5 2, 3.5 4),而SQL中的ROUND方法向上圆整。
已经实现的
Methods of form To<Type1>(<Type2> x) where Type1, Type2 is one of:
To<Type1>(<Type2> x)方法,但是Type1, Type2为下面类型中的一种:
bool, byte, char, DateTime, decimal, double, float, Int16, Int32, Int64, string.
这种方式类似于强制类型转换:
ToString(double)需要专门编写代码用于获取足够的精度。
For conversion int / char, DLinq uses SQL’s UNICODE / NCHAR function
int / char之间的转换,DLinq直接使用SQL的 UNICODE / NCHAR
其他方法则翻译成CONVERT方法。
没有实现的
在SQL中不存在的类型:ToSByte, UInt16, 32, 64
int To<int type>(string value, int fromBase), ToString(... value, int
toBase)
bool IsDBNull(object value)
TypeCode GetTypeCode(object value);
object ChangeType(...);
使用IFormatProvider作为参数的函数。
涉及到数组的方法(To/FromBase64CharArray, To/FromBase64String)
已经实现的
构造函数:
TimeSpan(long ticks)
TimeSpan (year, month, day)
TimeSpan (year, month, day, hour, minutes, seconds)
TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
重载操作符:
Comparison operators (<,==, etc.)
+, -
静态方法:
Compare(t1,t2)
非静态方法/属性:
Ticks, Milliseconds, Seconds, Hours, Days
TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays,
Equals, CompareTo(TimeSpan value)
Add(TimeSpan), Subtract(TimeSpan)
Duration() [= ABS], Negate()
没有实现的:
ToString()
Static TimeSpan FromDay(double value), FromHours, …
static TimeSpan Parse(string s)
已经实现的
构造函数:
DateTime(year, month, day)
DateTime(year, month, day, hour, minutes, seconds)
DateTime(year, month, day, hour, minutes, seconds, milliseconds)
重造操作符:
比较
DateTime – DateTime (gives TimeSpan),
DateTime + TimeSpan (gives DateTime)
DateTime – TimeSpan (gives DateTime)
静态方法:
Add (TimeSpan), AddTicks(long),
AddDays/Hours/Milliseconds/Minutes (double),
AddMonths/Years(int)
Equals
非静态方法/属性:
Day, Month, Year, Hour, Minute, Second, Millisecond
DayOfWeek
int CompareTo(DateTime value)
TimeSpan TimeOfDay()
Equals, ToString()
与.NET的区别
SQL中的datetime值圆整为.000, .003 或.007s,.NET的精度更高。
SQL中的datetime初始值为1753年1月1日。
SQL中内建的TimeSpan类型,SQL使用DATEDIFF方法返回一个32bit的整数。DATEDIFF(DAY,…)方法返回日数;DATEDIFF(MILLISECOND,…),返回秒数。DateTimes值不能大于24天,否则会出错。相对而言,.NET使用64bit的整数来表示TimeSpans的值。
为了使.NET和SQL中语意更加接近,DLinq会将TimeSpans转换为64bit的整数,使用上面提到的两个DATEDIFF方法来计算两个时间的滴答数。
当翻译某个查询为SQL的时候,DateTime.UtcNow 的值在客户端被确定(类似其他的于数据库数据无关的表达式)。
没有实现的
bool IsDaylightSavingTime();
bool IsLeapYear(int year);
int DaysInMonth(int year, int month);
long ToBinary();
long ToFileTime();
long ToFileTimeUtc();
string ToLongDateString();
string ToLongTimeString();
double ToOADate();
string ToShortDateString();
string ToShortTimeString();
DateTime ToUniversalTime();
DateTime FromBinary(long dateData), FileTime, FileTimeUtc, OADate
string[] GetDateTimeFormats(...)
constructor DateTime(long ticks);
Parse(string)
property DayOfYear
DataContext提供了获取查询产生的SQL以及改变处理逻辑的方法和属性。使用这些方法将便于对DLinq的功能的理解,而且可以用来调试某些特殊的问题。
成员名称 |
目的 |
Log |
在执行之前,打印SQL语句。包括查询、插入、更新、删除命令,用法如下: db.Log = Console.Out; |
GetQueryText(query) |
返回查询文本,而不是执行查询。用法如下: Console.WriteLine(db.GetQueryText(db.Customers)); |
GetChangeText() |
返回插入/更新/删除SQL命令的文本,而不是执行。用法如下: Console.WriteLine(db.GetChangeText()); |
您可以使用这个作为一个可视化帮助工具来细化您的查询。这个查询可视化支持工具可用于数据库查询,也可以得到相应的SQL语句。
在默认情况下,可视化查询工具会构建一段SQL文本来执行等同的数据库指令。您可以修改和执行这些SQL指令,返回的结果为一个Grid View。可视化查询工具在执行整个查询的时候会使用一个相同的连接字符串。
例如,输入下面对Northwind数据库的查询:
static void Main(string[] args)
{
Northwnd db = new Northwnd(
@"C:/program files/linq preview/data/northwnd.mdf");
var q =
from c in db.Customers
where c.City == " London "
select c;
} //在这里设置一个断点
当您的程序运行到这个断点的时候,打开“监视“窗口查看”q“或者将鼠标移动到”q“下查看提示信息,点击那个放大镜图标,就可以打开查询可视化查看器。
点击执行按钮,您将得到下面的结果:
选择““Original query”,您就可以看到原始 的SQL查询文本。
在这里,您可以看到原始的查询文本(@p0等等),每个参数都有一个CLR类型,SQL类型以及相应的值。但在这种模式下,您不能够修改查询文本。
您可以来回切换“Original query”来修改查询文本,这些修改将会被保存起来。您可以使用这个特性来比较原始查询和修改后的查询的结果。
有可能会因为某个类型替换为文本后而导致的类型信息的丢失而导致SQL形式的查询结果和原始的DLinq查询的结果会不一致,当然这种情况是很少见。Dlinq查询产生的结果往往和实际查询的结果是完全一致的。下面这个例子,针对Northwind数据库进行查询,最终的结果同预想的会有差别:
double x = 1;
var q = db.Customers.Select(c => (c.Orders.Count + x).ToString() );
在这个例子中,“x”进行字符串转换后为“1”,但是产生的原始SQL中“x”为一个浮点数,将得到一个不同的字符串。
如果DLinq查询产生两个SQL查询,查询查看器中将出现“Query 1” 和 “Query 2”选项。您可以任意地选择执行您要执行的SQL语句。您也可以修改它们,来回切换“Original query”选项来查看修改前和修改后的查询结果。
“Execute”按钮将执行这两个查询,并且可以分别显示这两个查询产生的结果。
static void Main(string[] args)
{
Northwnd db = new Northwnd(
@"C:/program files/linq preview/data/northwnd.mdf");
var q2 =
from c in db.Customers
where c.Country == "France"
group new {c.PostalCode, c.ContactName} by c.City into g
select new {g.Key,g};
} //在这里设置一个断点
结果显示为:
第一查询返回每个组的条目数,第二个查询返回数据。
到目前为止,如果一个DLinq查询产生两个或者多个SQL查询(即查询结果产生的组还存在多个组),那么查询查看器只能显示头两个查询的结果。