您的位置:首页 > 数据库

一步一步学Linq to sql(一)

2013-01-18 15:58 281 查看

(一):预备知识

什么是Linq to sql

Linq to sql(或者叫DLINQ)是LINQ(.NET语言集成查询)的一部分,全称基于关系数据的 .NET 语言集成查询,用于以对象形式管理关系数据,并提供了丰富的查询功能,它和Linq to xml、Linq to objects、Linq to dataset、Linq to entities等组成了强大的LINQ。

要学好LINQ查询语法,就不得不先理解C# 3.0的一些新特性,下面一一简单介绍。

隐含类型局部变量

var age = 26;
var username =
"zhuye";
var userlist =
new [] {"a","b","c"};
foreach(var user
in userlist)
Console.WriteLine(user);

纯粹给懒人用的var关键字,告诉编译器(对于CLR来说,它是不会知道你是否使用了var,苦力是编译器出的),你自己推断它的类型吧,我不管了。但是既然让编译器推断类型就必须声明的时候赋值,而且不能是null值。注意,这只能用于局部变量,用于字段是不可以的。

匿名类型

var data =
new {username = "zhuye",age = 26};
Console.WriteLine("username:{0} age:{1}", data.username, data.age);

匿名类型允许开发人员定义行内类型,无须显式定义类型。常和var配合使用,var用于声明匿名类型。定义一个临时的匿名类型在LINQ查询句法中非常常见,我们可以很方便的实现对象的转换和投影。

扩展方法

public static
class helper
{
public
static string MD5Hash(this
string s)
{
return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(s,"MD5");
}
public
static bool In(this
object o, IEnumerable b)
{
foreach(object obj
in b)
{
if(obj==o)
return
true;
}
return
false;
}
}

// 调用扩展方法
Console.WriteLine("123456".MD5Hash());
Console.WriteLine("1".In(new[]{"1","2","3"}));
很多时候我们需要对CLR类型进行一些操作,苦于无法扩展CLR类型的方法,只能创建一些helper方法,或者生成子类。扩展方法使得这些需求得意实现,同时也是实现LINQ的基础。定义扩展方法需要注意,只能在静态类中定义并且是静态方法,如果扩展方法名和原有方法名发生冲突,那么扩展方法将失效。

自动属性

public class
Person
{
public
string username { get;
protected set; }
public
int age { get; set; }
public Person()
{
this.username =
"zhuye";
}
}

Person p =
new Person();
//p.username = "aa";
Console.WriteLine(p.username);

意义不是很大,纯粹解决机械劳动。编译器自动为你生成get、set操作以及字段,并且你不能使用字段也不能自定义get、set操作,不过你可以分别定义get和set的访问级别。

对象初始化器

public class
Person
{
public
string username { get;
set; }
public
int age { get; set; }

public
override string ToString()
{
return
string.Format("username:{0} age:{1}",
this.username, this.age);
}
}

Person p =
new Person() {username =
"zhuye", age=26};
Console.WriteLine(p.ToString());
编译器会自动为你做setter操作,使得原本几行的属性赋值操作能在一行中完成。这里需要注意:

允许只给一部分属性赋值,包括internal访问级别
可以结合构造函数一起使用,并且构造函数初始化先于对象初始化器执行

集合初始化器

public class
Person
{
public
string username { get;
set; }
public
int age { get; set; }

public
override string ToString()
{
return
string.Format("username:{0} age:{1}",
this.username, this.age);
}
}

var persons =
new List<Person> {
new
Person {username = "a", age=1},
new
Person {username = "b", age=2}};
foreach(var p
in persons)
Console.WriteLine(p.ToString());
编译器会自动为你做集合插入操作。如果你为Hashtable初始化的话就相当于使用了两个对象初始化器。

Lambda表达式

var list =
new [] { "aa",
"bb", "ac" };
var result =
Array.FindAll(list, s => (s.IndexOf("a") > -1));
foreach (var v
in result)
Console.WriteLine(v);

其实和2.0中的匿名方法差不多,都是用于产生内联方法,只不过Lambda表达式的语法更为简洁。语法如下:

(参数列表) => 表达式或者语句块

其中:

参数个数:可以有多个参数,一个参数,或者无参数。

表达式或者语句块:这部分就是我们平常写函数的实现部分(函数体)。

前面的示例分别是1个参数的例子,下面结合扩展方法来一个复杂的例子:

public delegate
int mydg(int a,
int b);
public
static class LambdaTest
{
public
static int oper(this
int a, int b,
mydg dg)
{
return dg(a, b);
}
}

Console.WriteLine(1.oper(2, (a, b) => a + b));
Console.WriteLine(2.oper(1, (a, b) => a - b));

查询句法

var persons =
new List<Person> {
new
Person {username = "a", age=19},
new
Person {username = "b", age=20},
new
Person {username = "a", age=21},
};
var selectperson =
from p in persons
where p.age >= 20 select p.username.ToUpper();
foreach(var p
in selectperson)
Console.WriteLine(p);

查询句法是使用标准的LINQ查询运算符来表达查询时一个方便的声明式简化写法。该句法能在代码里表达查询时增进可读性和简洁性,读起来容易,也容易让人写对。Visual Studio 对查询句法提供了完整的智能感应和编译时检查支持。编译器在底层把查询句法的表达式翻译成明确的方法调用代码,代码通过新的扩展方法和Lambda表达式语言特性来实现。上面的查询句法等价于下面的代码:

var selectperson = persons.Where(p=>p.age>=20).Select(p=>p.username.ToUpper());

LINQ查询句法可以实现90%以上T-SQL的功能(由于T-SQL是基于二维表的,所以LINQ的查询语法会比T-SQL更简单和灵活),但是由于智能感应的原因,select不能放在一开始就输入。

(二):DataContext与实体

DataContext

DataContext类型(数据上下文)是System.Data.Linq命名空间下的重要类型,用于把查询句法翻译成SQL语句,以及把数据从数据库返回给调用方和把实体的修改写入数据库。

DataContext提供了以下一些使用的功能:

以日志形式记录DataContext生成的SQL
执行SQL(包括查询和更新语句)
创建和删除数据库

DataContext是实体和数据库之间的桥梁,那么首先我们需要定义映射到数据表的实体。

定义实体类

using System.Data.Linq.Mapping;

[Table(Name = "Customers")]

public class
Customer

{

[Column(IsPrimaryKey =
true)]

public string CustomerID {get;
set;}

[Column(Name =
"ContactName")]

public string Name {
get; set; }

[Column]

public string City {get;
set;}

}

以Northwind数据库为例,上述Customers类被映射成一个表,对应数据库中的 Customers表。然后在类型中定义了三个属性,对应表中的三个字段。其中,CustomerID字段是主键,如果没有指定Column特性的Name属性,那么系统会把属性名作为数据表的字段名,也就是说实体类的属性名就需要和数据表中的字段名一致。

现在,创建一个ASP.NET页面,然后在页面上加入一个GridView控件,使用下面的代码进行绑定数据:

using System.Data.Linq;

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

Table<Customer> Customers = ctx.GetTable<Customer>();

GridView1.DataSource = from c
in Customers where c.CustomerID.StartsWith("A")
select new {顾客ID=c.CustomerID, 顾客名=c.Name, 城市=c.City};

GridView1.DataBind();

使用DataContext类型把实体类和数据库中的数据进行关联。你可以直接在DataContext的构造方法中定义连接字符串,也可以使用IDbConnection:

using System.Data.SqlClient;

IDbConnection conn =
new SqlConnection("server=xxx;database=Northwind;uid=xxx;pwd=xxx");

DataContext ctx =
new DataContext(conn);

之后,通过GetTable获取表示底层数据表的Table类型,显然,数据库中的Customers表的实体是Customer类型。随后的查询句法,即使你不懂SQL应该也能看明白。从Customers表中找出CustomerID以“A”开头的记录,并把CustomersID、Name以及City封装成新的匿名类型进行返回。

结果如下图:

强类型DataContext

public partial
class NorthwindDataContext :
DataContext

{

public Table<Customer> Customers;

public NorthwindDataContext(IDbConnection connection) :
base(connection) { }

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

}

强类型数据上下文使代码更简洁:

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

GridView1.DataSource = from c
in ctx.Customers where c.CustomerID.StartsWith("A")
select new { 顾客ID = c.CustomerID, 顾客名 = c.Name, 城市 = c.City };

GridView1.DataBind();

DataContext其实封装了很多实用的功能,下面一一介绍。

日志功能

using System.IO;

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

StreamWriter sw =
new StreamWriter(Server.MapPath("log.txt"),
true); // Append

ctx.Log = sw;

GridView1.DataSource = from c
in ctx.Customers where c.CustomerID.StartsWith("A")
select new { 顾客ID = c.CustomerID, 顾客名 = c.Name, 城市 = c.City };

GridView1.DataBind();

sw.Close();

运行程序后在网站所在目录生成了log.txt,每次查询都会把诸如下面的日志追加到文本文件中:

SELECT [t0].[CustomerID], [t0].[ContactName], [t0].[City]

FROM [Customers] AS [t0]

WHERE [t0].[CustomerID] LIKE @p0

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

-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

应该说这样的日志对于调试程序是非常有帮助的。

探究查询

using System.Data.Common;

using System.Collections.Generic;

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

var select = from c
in ctx.Customers where c.CustomerID.StartsWith("A")
select new { 顾客ID = c.CustomerID, 顾客名 = c.Name, 城市 = c.City };

DbCommand cmd = ctx.GetCommand(select);

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

foreach (DbParameter parm
in cmd.Parameters)

Response.Write(string.Format("参数名:{0},参数值:{1}<br/>",
parm.ParameterName, parm.Value));

Customer customer = ctx.Customers.First();

customer.Name = "zhuye";

IList<object> queryText = ctx.GetChangeSet().ModifiedEntities;

Response.Write(((Customer)queryText[0]).Name);

在这里,我们通过DataContext的GetCommand方法获取了查询对应的DbCommand,并且输出了CommandText和所有的DbParameter。之后,我们又通过GetChangeSet方法获取了修改后的实体,并输出了修改内容。

执行查询

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

string newcity = "Shanghai";

ctx.ExecuteCommand("update Customers set City={0} where CustomerID like 'A%'", newcity);

IEnumerable<Customer> customers = ctx.ExecuteQuery<Customer>("select * from Customers where CustomerID like 'A%'");

GridView1.DataSource = customers;

GridView1.DataBind();

前一篇文章已经说了,虽然Linq to sql能实现90%以上的TSQL功能。但是不可否认,对于复杂的查询,使用TSQL能获得更好的效率。因此,DataContext类型也提供了执行SQL语句的能力。代码的执行结果如下图:

创建数据库

testContext ctx =
new testContext("server=xxx;database=testdb;uid=xxx;pwd=xxx");

ctx.CreateDatabase();

[Table(Name = "test")]

public class
test

{

[Column(IsPrimaryKey =
true, IsDbGenerated = true)]

public int ID {
get; set; }

[Column(DbType="varchar(20)")]

public string Name {
get; set; }

}

public partial
class testContext :
DataContext

{

public Table<test> test;

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

}

这段代码在数据库中创建了名为testdb的数据库,等同于下面的脚本:

CREATE TABLE [dbo].[test](

[ID] [int] IDENTITY(1,1)
NOT NULL,

[Name] [varchar](20)
COLLATE Chinese_PRC_CI_AS
NULL,

CONSTRAINT [PK_test]
PRIMARY KEY CLUSTERED

(

[ID] ASC

)WITH
(IGNORE_DUP_KEY =
OFF) ON [PRIMARY]

) ON [PRIMARY]

同时,DataContext还提供了DeleteDatabase()方法,在这里就不列举了。

使用DbDataReader数据源

using System.Data.SqlClient;

var conn = new
SqlConnection("server=xxx;database=Northwind;uid=xxx;pwd=xxx");

var ctx = new
DataContext(conn);

var cmd = new
SqlCommand("select * from customers where CustomerID like 'A%'", conn);

conn.Open();

var reader = cmd.ExecuteReader();

GridView1.DataSource = ctx.Translate<Customer>(reader);

GridView1.DataBind();

conn.Close();

你同样可以选择使用DataReader获取数据,增加了灵活性的同时也增加了性能。

看到这里,你可能会觉得手工定义和数据库中表对应的实体类很麻烦,不用担心,VS2008提供了自动生成实体类以及关系的工具,工具的使用将在以后讲解。今天就讲到这里,和DataContext相关的事务、加载选项、并发选项以及关系实体等高级内容也将在以后讲解。

(三):增删改

示例数据库

字段名

字段类型

允许空

字段说明

ID

uniqueidentifier

表主键字段

UserName

varchar(50)

留言用户名

PostTime

datetime

留言时间

Message

varchar(400)



留言内容

IsReplied

bit

留言是否回复

Reply

varchar(400)



留言管理员回复

在数据库中创建一个名为GuestBook的数据库,在里面创建一个tbGuestBook的表,结构如上表。

生成实体类

右键点击网站项目,选择添加新项,然后选择“Linq to sql Classes”,命名为GuestBook。然后打开App_Code里面的GuestBook.dbml。设计视图上的文字提示你可以从服务器资源管理器或者攻击箱拖动项到设计界面上来创建实体类。

那么,我们就在服务器资源管理器中创建一个指向GuestBook数据库的数据连接,然后把tbGuestBook表拖动到GuestBook.dbml的设计视图上,按CTRL+S保存。打开GuestBook.designer.cs可以发现系统自动创建了GuestBook数据库中tbGuestBook表的映射,如下图:

简易留言簿

现在,我们就可以使用Linq to sql完成简易留言簿了。实现以下功能:

发表留言(增)
查看留言(查)
管理员回复留言(改)
管理员删除留言(删除)

首先,创建一个Default.aspx,在页面上加入一些控件:

<div>
姓名
<asp:TextBox
ID="tb_UserName"
runat="server"></asp:TextBox><br
/>
<br
/>
留言
<asp:TextBox
ID="tb_Message"
runat="server" Height="100px"
TextMode="MultiLine"
Width="300px"></asp:TextBox><br
/>
<br
/>
<asp:Button
ID="btn_SendMessage"
runat="server"
Text="发表留言"
OnClick="btn_SendMessage_Click"
/><br
/>
<br
/>
<asp:Repeater
ID="rpt_Message"
runat="server">
<ItemTemplate>
<table
width="600px"
style="border:solid
1px #666666;
font-size:10pt; background-color:#f0f0f0">
<tr>
<td
align="left"
width="400px">
<%# Eval("Message")%>
</td>
<td
align="right"
width="200px">
<%# Eval("PostTime")%> -
<%# Eval("UserName")%>
</td>
</tr>
<tr>
<td
colspan="2"
align="right">
<hr
width="300px"
/>
管理员回复:<%# Eval("IsReplied").ToString() ==
"False" ? "暂无" : Eval("Reply")%>
</td>
</tr>

</table>
<br/>
</ItemTemplate>
</asp:Repeater>
</div>

你可能很难想象,使用Linq to sql进行数据访问会是这么简单,后台代码:

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

{
GuestBookDataContext ctx =
new GuestBookDataContext("server=xxx;database=GuestBook;uid=xxx;pwd=xxx");

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.UserName = tb_UserName.Text;
gb.Message = tb_Message.Text;
gb.IsReplied = false;
gb.PostTime = DateTime.Now;
ctx.tbGuestBooks.Add(gb);
ctx.SubmitChanges();
SetBind();
}
private
void SetBind()
{
rpt_Message.DataSource = from gb
in ctx.tbGuestBooks orderby gb.PostTime
descending select gb;
rpt_Message.DataBind();
}
}

前面创建Linq to sql Classes的时候我们输入名字GuestBook,系统就为我们自动创建了GuestBookDataContext(你也可以在GuestBook.Designer.cs中找到类定义)。在绑定的时候我们使用查询句法查询留言表中所有留言,按照发表时间倒序(天哪?这是数据访问吗?好像仅仅定义了一句SQL啊)。在发表留言按钮中,我们为一个tbGuestBook赋值,然后把它加入留言表,再提交更改,就这样完成了记录的插入。

运行效果如下图:

然后,再创建一个Admin.aspx,前台代码如下:

<div>
<asp:Repeater
ID="rpt_Message"
runat="server" OnItemCommand="rpt_Message_ItemCommand">
<ItemTemplate>
<table
width="600px"
style="border:solid
1px #666666;
font-size:10pt; background-color:#f0f0f0">
<tr>
<td
align="left"
width="400px">
<%# Eval("Message")%>
</td>
<td
align="right"
width="200px">
<%# Eval("PostTime")%> -
<%# Eval("UserName")%>
</td>
</tr>
<tr>
<td
colspan="2"
align="right">
<hr
width="300px"
/>
<asp:Button
ID="btn_DeleteMessage"
runat="server"
Text="删除留言"
CommandName="DeleteMessage"
CommandArgument='<%# Eval("ID")%>'/>
管理员回复:<asp:TextBox
runat="server"
ID="tb_Reply" TextMode="MultiLine"
Width="300px"
Text='<%# Eval("Reply")%>'/>
<asp:Button
ID="btn_SendReply"
runat="server"
Text="发表回复"
CommandName="SendReply"
CommandArgument='<%# Eval("ID")%>'/>
</td>
</tr>

</table>
<br/>
</ItemTemplate>
</asp:Repeater>
</div>

后台代码:

public partial
class Admin : System.Web.UI.Page
{
GuestBookDataContext ctx =
new GuestBookDataContext("server=xxx;database=GuestBook;uid=xxx;pwd=xxx");

protected
void Page_Load(object sender,
EventArgs e)
{
if (!IsPostBack)
{
SetBind();
}
}

private
void SetBind()
{
rpt_Message.DataSource = from gb
in ctx.tbGuestBooks orderby gb.PostTime
descending select gb;
rpt_Message.DataBind();
}
protected
void rpt_Message_ItemCommand(object source,
RepeaterCommandEventArgs e)
{
if (e.CommandName ==
"DeleteMessage")
{
StreamWriter sw =
new StreamWriter(Server.MapPath("log.txt"),
true);
ctx.Log = sw;
tbGuestBook gb = ctx.tbGuestBooks.Single(b => b.ID ==
new Guid(e.CommandArgument.ToString()));
ctx.tbGuestBooks.Remove(gb);
ctx.SubmitChanges();
SetBind();
sw.Close();
}
if (e.CommandName ==
"SendReply")
{
StreamWriter sw =
new StreamWriter(Server.MapPath("log.txt"),
true);
ctx.Log = sw;
tbGuestBook gb = ctx.tbGuestBooks.Single(b => b.ID ==
new Guid(e.CommandArgument.ToString()));
gb.Reply = ((TextBox)e.Item.FindControl("tb_Reply")).Text;
gb.IsReplied = true;
ctx.SubmitChanges();
SetBind();
sw.Close();
}
}
}
运行效果如下图:

在这里,我们通过Single方法获取一条记录,也就是一个tbGuestBook实例,更新了一些属性后保存也就完成了改这个操作。删除操作更简单,只需要从表中移除对象。你是不是觉得好像不是在操作数据库,像在操作内存中的对象。

由于写了日志,看看改和删操作会是怎么样的SQL?

UPDATE [dbo].[tbGuestBook]
SET [IsReplied] = @p4, [Reply] = @p5
WHERE ([ID] = @p0) AND ([UserName] = @p1) AND ([PostTime] = @p2) AND ([Message] = @p3) AND (NOT ([IsReplied] = 1)) AND ([Reply] IS NULL)
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [00000000-0000-0000-0000-000000000000]
-- @p1: Input String (Size = 4; Prec = 0; Scale = 0) [ghgh]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2007-8-16 10:20:09]
-- @p3: Input String (Size = 3; Prec = 0; Scale = 0) [ghj]
-- @p4: Input Boolean (Size = 0; Prec = 0; Scale = 0) [True]
-- @p5: Input String (Size = 3; Prec = 0; Scale = 0) [qqq]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

DELETE FROM [dbo].[tbGuestBook] WHERE ([ID] = @p0) AND ([UserName] = @p1) AND ([PostTime] = @p2) AND ([Message] = @p3) AND (NOT ([IsReplied] = 1)) AND ([Reply] = @p4)
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [158ec941-13ff-4093-bd8b-9fceae152171]
-- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [44]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2007-8-16 9:56:19]
-- @p3: Input String (Size = 2; Prec = 0; Scale = 0) [44]
-- @p4: Input String (Size = 3; Prec = 0; Scale = 0) [222]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

(四):查询句法

select

描述:查询顾客的公司名、地址信息

查询句法:

var 构建匿名类型1 =
from c in ctx.Customers
select
new
{
公司名 = c.CompanyName,
地址 = c.Address
};

对应SQL:

SELECT [t0].[CompanyName], [t0].[Address]
FROM [dbo].[Customers] AS [t0]
描述:查询职员的姓名和雇用年份

查询句法:

var 构建匿名类型2 =
from emp in ctx.Employees
select
new
{
姓名 = emp.LastName + emp.FirstName,
雇用年 = emp.HireDate.Value.Year
};

对应SQL:

SELECT [t0].[LastName] + [t0].[FirstName] AS [value], DATEPART(Year, [t0].[HireDate]) AS [value2]
FROM [dbo].[Employees] AS [t0]
描述:查询顾客的ID以及联系信息(职位和联系人)

查询句法:

var 构建匿名类型3 =
from c in ctx.Customers
select
new
{
ID = c.CustomerID,
联系信息 = new
{
职位 = c.ContactTitle,
联系人 = c.ContactName
}
};

对应SQL:

SELECT [t0].[CustomerID], [t0].[ContactTitle], [t0].[ContactName]
FROM [dbo].[Customers] AS [t0]
描述:查询订单号和订单是否超重的信息

查询句法:

var select带条件 =
from o in ctx.Orders
select
new
{
订单号 = o.OrderID,
是否超重 = o.Freight > 100 ?
"是" :
"否"
};

对应SQL:

SELECT [t0].[OrderID],
(CASE
WHEN [t0].[Freight] > @p0 THEN @p1
ELSE @p2
END) AS [value]
FROM [dbo].[Orders] AS [t0]
-- @p0: Input Currency (Size = 0; Prec = 19; Scale = 4) [100]
-- @p1: Input String (Size = 1; Prec = 0; Scale = 0) [是]
-- @p2: Input String (Size = 1; Prec = 0; Scale = 0) [否]
where

描述:查询顾客的国家、城市和订单数信息,要求国家是法国并且订单数大于5

查询句法:

var 多条件 =
from c in ctx.Customers
where c.Country ==
"France" && c.Orders.Count > 5
select
new
{
国家 = c.Country,
城市 = c.City,
订单数 = c.Orders.Count
};

对应SQL:

SELECT [t0].[Country], [t0].[City], (
SELECT COUNT(*)
FROM [dbo].[Orders] AS [t2]
WHERE [t2].[CustomerID] = [t0].[CustomerID]
) AS [value]
FROM [dbo].[Customers] AS [t0]
WHERE ([t0].[Country] = @p0) AND (((
SELECT COUNT(*)
FROM [dbo].[Orders] AS [t1]
WHERE [t1].[CustomerID] = [t0].[CustomerID]
)) > @p1)
-- @p0: Input String (Size = 6; Prec = 0; Scale = 0) [France]
-- @p1: Input Int32 (Size = 0; Prec = 0; Scale = 0) [5]
orderby

描述:查询所有没有下属雇员的雇用年和名,按照雇用年倒序,按照名正序

查询句法:

var 排序 =
from emp in ctx.Employees
where emp.Employees.Count == 0
orderby emp.HireDate.Value.Year
descending, emp.FirstName
ascending
select
new
{
雇用年 = emp.HireDate.Value.Year,
名 = emp.FirstName
};

对应SQL:

SELECT DATEPART(Year, [t0].[HireDate]) AS [value], [t0].[FirstName]
FROM [dbo].[Employees] AS [t0]
WHERE ((
SELECT COUNT(*)
FROM [dbo].[Employees] AS [t1]
WHERE [t1].[ReportsTo] = [t0].[EmployeeID]
)) = @p0
ORDER BY DATEPART(Year, [t0].[HireDate]) DESC, [t0].[FirstName]
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [0]
分页

描述:按照每页10条记录,查询第二页的顾客

查询句法:

var 分页 = (from c
in ctx.Customers select c).Skip(10).Take(10);

对应SQL:

SELECT TOP 10 [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode], [t0].[Country], [t0].[Phone], [t0].[Fax]) AS [ROW_NUMBER],
[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]
) AS [t1]
WHERE [t1].[ROW_NUMBER] > @p0
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [10]
分组

描述:根据顾客的国家分组,查询顾客数大于5的国家名和顾客数

查询句法:

var 一般分组 =
from c in ctx.Customers
group c
by c.Country into g
where g.Count() > 5
orderby g.Count()
descending
select
new
{
国家 = g.Key,
顾客数 = g.Count()
};

对应SQL:

SELECT [t1].[Country], [t1].[value3] AS [顾客数]
FROM (
SELECT COUNT(*) AS [value], COUNT(*) AS [value2], COUNT(*) AS [value3], [t0].[Country]
FROM [dbo].[Customers] AS [t0]
GROUP BY [t0].[Country]
) AS [t1]
WHERE [t1].[value] > @p0
ORDER BY [t1].[value2] DESC
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [5]
描述:根据国家和城市分组,查询顾客覆盖的国家和城市

查询句法:

var 匿名类型分组 =
from c in ctx.Customers
group c
by new { c.City, c.Country }
into g
orderby g.Key.Country, g.Key.City
select
new
{
国家 = g.Key.Country,
城市 = g.Key.City
};

对应SQL:

SELECT [t1].[Country], [t1].[City]
FROM (
SELECT [t0].[City], [t0].[Country]
FROM [dbo].[Customers] AS [t0]
GROUP BY [t0].[City], [t0].[Country]
) AS [t1]
ORDER BY [t1].[Country], [t1].[City]
描述:按照是否超重条件分组,分别查询订单数量

查询句法:

var 按照条件分组 =
from o in ctx.Orders
group o
by new { 条件 = o.Freight > 100 }
into g
select
new
{
数量 = g.Count(),
是否超重 = g.Key.条件 ?
"是" :
"否"
};

对应SQL:

SELECT
(CASE
WHEN [t2].[value2] = 1 THEN @p1
ELSE @p2
END) AS [value], [t2].[value] AS [数量]
FROM (
SELECT COUNT(*) AS [value], [t1].[value] AS [value2]
FROM (
SELECT
(CASE
WHEN [t0].[Freight] > @p0 THEN 1
WHEN NOT ([t0].[Freight] > @p0) THEN 0
ELSE NULL
END) AS [value]
FROM [dbo].[Orders] AS [t0]
) AS [t1]
GROUP BY [t1].[value]
) AS [t2]
-- @p0: Input Currency (Size = 0; Prec = 19; Scale = 4) [100]
-- @p1: Input String (Size = 1; Prec = 0; Scale = 0) [是]
-- @p2: Input String (Size = 1; Prec = 0; Scale = 0) [否]
distinct

描述:查询顾客覆盖的国家

查询句法:

var 过滤相同项 = (from c
in ctx.Customers orderby c.Country
select c.Country).Distinct();

对应SQL:

SELECT DISTINCT [t0].[Country]
FROM [dbo].[Customers] AS [t0]
union

描述:查询城市是A打头和城市包含A的顾客并按照顾客名字排序

查询句法:

var 连接并且过滤相同项 = (from c
in ctx.Customers where c.City.Contains("A")
select c).Union
(from c in ctx.Customers
where c.ContactName.StartsWith("A")
select c).OrderBy(c => c.ContactName);

对应SQL:

SELECT [t3].[CustomerID], [t3].[CompanyName], [t3].[ContactName], [t3].[ContactTitle], [t3].[Address], [t3].[City], [t3].[Region], [t3].[PostalCode], [t3].[Country], [t3].[Phone], [t3].[Fax]
FROM (
SELECT [t2].[CustomerID], [t2].[CompanyName], [t2].[ContactName], [t2].[ContactTitle], [t2].[Address], [t2].[City], [t2].[Region], [t2].[PostalCode], [t2].[Country], [t2].[Phone], [t2].[Fax]
FROM (
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].[City] LIKE @p0
UNION
SELECT [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM [dbo].[Customers] AS [t1]
WHERE [t1].[ContactName] LIKE @p1
) AS [t2]
) AS [t3]
ORDER BY [t3].[ContactName]
-- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [%A%]
-- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [A%]
concat

描述:查询城市是A打头和城市包含A的顾客并按照顾客名字排序,相同的顾客信息不会过滤

查询句法:

var 连接并且不过滤相同项 = (from c
in ctx.Customers where c.City.Contains("A")
select c).Concat
(from c in ctx.Customers
where c.ContactName.StartsWith("A")
select c).OrderBy(c => c.ContactName);

对应SQL:

SELECT [t3].[CustomerID], [t3].[CompanyName], [t3].[ContactName], [t3].[ContactTitle], [t3].[Address], [t3].[City], [t3].[Region], [t3].[PostalCode], [t3].[Country], [t3].[Phone], [t3].[Fax]
FROM (
SELECT [t2].[CustomerID], [t2].[CompanyName], [t2].[ContactName], [t2].[ContactTitle], [t2].[Address], [t2].[City], [t2].[Region], [t2].[PostalCode], [t2].[Country], [t2].[Phone], [t2].[Fax]
FROM (
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].[City] LIKE @p0
UNION ALL
SELECT [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM [dbo].[Customers] AS [t1]
WHERE [t1].[ContactName] LIKE @p1
) AS [t2]
) AS [t3]
ORDER BY [t3].[ContactName]
-- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [%A%]
-- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [A%]
取相交项

描述:查询城市是A打头的顾客和城市包含A的顾客的交集,并按照顾客名字排序

查询句法:

var 取相交项 = (from c
in ctx.Customers where c.City.Contains("A")
select c).Intersect
(from c in ctx.Customers
where c.ContactName.StartsWith("A")
select c).OrderBy(c => c.ContactName);

对应SQL:

SELECT [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM (
SELECT DISTINCT [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]
) AS [t1]
WHERE (EXISTS(
SELECT NULL AS [EMPTY]
FROM [dbo].[Customers] AS [t2]
WHERE ([t1].[CustomerID] = [t2].[CustomerID]) AND ([t2].[ContactName] LIKE @p0)
)) AND ([t1].[City] LIKE @p1)
ORDER BY [t1].[ContactName]
-- @p0: Input String (Size = 2; Prec = 0; Scale = 0) [A%]
-- @p1: Input String (Size = 3; Prec = 0; Scale = 0) [%A%]
排除相交项

描述:查询城市包含A的顾客并从中删除城市以A开头的顾客,并按照顾客名字排序

查询句法:

var 排除相交项 = (from c
in ctx.Customers where c.City.Contains("A")
select c).Except
(from c in ctx.Customers
where c.ContactName.StartsWith("A")
select c).OrderBy(c => c.ContactName);

对应SQL:

SELECT [t1].[CustomerID], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Address], [t1].[City], [t1].[Region], [t1].[PostalCode], [t1].[Country], [t1].[Phone], [t1].[Fax]
FROM (
SELECT DISTINCT [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]
) AS [t1]
WHERE (NOT (EXISTS(
SELECT NULL AS [EMPTY]
FROM [dbo].[Customers] AS [t2]
WHERE ([t1].[CustomerID] = [t2].[CustomerID]) AND ([t2].[ContactName] LIKE @p0)
))) AND ([t1].[City] LIKE @p1)
ORDER BY [t1].[ContactName]
-- @p0: Input String (Size = 2; Prec = 0; Scale = 0) [A%]
-- @p1: Input String (Size = 3; Prec = 0; Scale = 0) [%A%]
子查询

描述:查询订单数超过5的顾客信息

查询句法:

var 子查询 =
from c in ctx.Customers
where
(from o
in ctx.Orders group o
by o.CustomerID into o
where o.Count() > 5 select o.Key).Contains(c.CustomerID)
select 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]
WHERE EXISTS(
SELECT NULL AS [EMPTY]
FROM (
SELECT COUNT(*) AS [value], [t1].[CustomerID]
FROM [dbo].[Orders] AS [t1]
GROUP BY [t1].[CustomerID]
) AS [t2]
WHERE ([t2].[CustomerID] = [t0].[CustomerID]) AND ([t2].[value] > @p0)
)
-- @p0: Input Int32 (Size = 0; Prec = 0; Scale = 0) [5]
in操作

描述:查询指定城市中的客户

查询句法:

var in操作 =
from c in ctx.Customers
where
new string[] {
"Brandenburg", "Cowes",
"Stavern" }.Contains(c.City)
select 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]
WHERE [t0].[City] IN (@p0, @p1, @p2)
-- @p0: Input String (Size = 11; Prec = 0; Scale = 0) [Brandenburg]
-- @p1: Input String (Size = 5; Prec = 0; Scale = 0) [Cowes]
-- @p2: Input String (Size = 7; Prec = 0; Scale = 0) [Stavern]
join

描述:内连接,没有分类的产品查询不到

查询句法:

var innerjoin =
from p in ctx.Products
join c
in ctx.Categories
on p.CategoryID
equals c.CategoryID
select p.ProductName;

对应SQL:

SELECT COUNT(*) AS [value]
FROM [dbo].[Products] AS [t0]
INNER JOIN [dbo].[Categories] AS [t1] ON [t0].[CategoryID] = ([t1].[CategoryID])
描述:外连接,没有分类的产品也能查询到

查询句法:

var leftjoin =
from p in ctx.Products
join c
in ctx.Categories
on p.CategoryID
equals c.CategoryID
into pro
from x
in pro.DefaultIfEmpty()
select p.ProductName;

对应SQL:

SELECT COUNT(*) AS [value]
FROM [dbo].[Products] AS [t0]
LEFT OUTER JOIN [dbo].[Categories] AS [t1] ON [t0].[CategoryID] = ([t1].[CategoryID])
你可能会很奇怪,原先很复杂的SQL使用查询句法会很简单(比如按照条件分组)。但是原先觉得很好理解的SQL使用查询句法会觉得很复杂(比如连接查询)。其实,我们还可以通过其它方式进行连接操作,在以后说DataLoadOptions类型的时候会再说。虽然Linq to sql已经非常智能了,但是对于非常复杂的查询还是建议通过存储过程实现,下次讲解如何调用存储过程。

(五):存储过程

普通存储过程

首先在查询分析器运行下面的代码来创建一个存储过程:

create proc sp_singleresultset
as
set nocount
on
select *
from customers
然后打开IDE的服务器资源管理器,之前我们从表中拖动表到dbml设计视图,这次我们从存储过程中找到刚才创建的存储过程,然后拖动到设计视图。在方法面板中可以看到已经创建了一个sp_singleresultset的方法,如下图:

然后打开Northwind.designer.cs,可以找到下面的代码:

[Function(Name="dbo.sp_singleresultset")]
public
ISingleResult<sp_singleresultsetResult> sp_singleresultset()
{
IExecuteResult result =
this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())));
return ((ISingleResult<sp_singleresultsetResult>)(result.ReturnValue));
}

我们可以发现,IDE为这个存储过程单独生成了返回结果集的实体定义,你可能会觉得很奇怪,IDE怎么知道这个存储过程将会返回哪些数据那?其实,在把存储过程拖拽入dbml设计视图的时候,IDE就执行了类似下面的命令:

SET FMTONLY ON;

exec Northwind.dbo.sp_singleresultset

SET FMTONLY OFF;

这样就可以直接获取存储过程返回的元数据而无须执行存储过程。

其实我们存储过程返回的就是顾客表的数据,如果你觉得为存储过程单独设置结果集实体有些浪费的话可以在存储过程的属性窗口中调整返回类型从“自动生成的类型”到Customer,不过以后你只能通过删除方法面板中的存储过程,然后重新添加来还原到“自动生成的类型”。下面,我们可以写如下的Linq to object代码进行查询:

var 单结果集存储过程 =
from c
in ctx.sp_singleresultset()
where c.CustomerID.StartsWith("A")
select c;

在这里确实是Linq to object的,因为查询句法不会被整句翻译成SQL,而是从存储过程的返回对象中再去对对象进行查询。SQL代码如下:

EXEC @RETURN_VALUE = [dbo].[sp_singleresultset]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []

带参数的存储过程

创建如下存储过程:

create proc [dbo].[sp_withparameter]
@customerid nchar(5),
@rowcount int
output
as
set nocount
on
set @rowcount =
(select
count(*) from customers
where customerid = @customerid)

使用同样的方法生成存储过程方法,然后使用下面的代码进行测试:

int? rowcount = -1;
ctx.sp_withparameter("",
ref rowcount);
Response.Write(rowcount);
ctx.sp_withparameter("ALFKI",
ref rowcount);
Response.Write(rowcount);

结果输出了“01”。说明ID为“”的顾客数为0,而ID为“ALFKI”的顾客数为1。存储过程的输出参数被封装成了ref参数,对于C#语法来说非常合情合理。SQL代码如下:

EXEC @RETURN_VALUE = [dbo].[sp_withparameter] @customerid = @p0, @rowcount = @p1 OUTPUT
-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) []
-- @p1: InputOutput Int32 (Size = 0; Prec = 0; Scale = 0) [-1]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []

带返回值的存储过程

再来创建第三个存储过程:

create proc [dbo].[sp_withreturnvalue]
@customerid nchar(5)
as
set nocount
on
if exists
(select 1
from customers where customerid
= @customerid)
return 101
else
return 100

生成方法后,可以通过下面的代码进行测试:

Response.Write(ctx.sp_withreturnvalue(""));
Response.Write(ctx.sp_withreturnvalue("ALFKI"));

运行后程序输出“100101”

多结果集的存储过程

再来创建一个多结果集的存储过程:

create proc [dbo].[sp_multiresultset]
as
set nocount
on
select *
from customers
select *
from employees

找到生成的存储过程方法:

[Function(Name="dbo.sp_multiresultset")]
public
ISingleResult<sp_multiresultsetResult> sp_multiresultset()
{
IExecuteResult result =
this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())));
return ((ISingleResult<sp_multiresultsetResult>)(result.ReturnValue));
}

由于现在的VS2008会把多结果集存储过程识别为单结果集存储过程(只认识第一个结果集),我们只能对存储过程方法多小动手术,修改为:

[Function(Name="dbo.sp_multiresultset")]
[ResultType(typeof(Customer))]
[ResultType(typeof(Employee))]
public
IMultipleResults sp_multiresultset()
{
IExecuteResult result =
this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())));
return (IMultipleResults)(result.ReturnValue);
}

然后使用下面的代码测试:

var 多结果集存储过程 = ctx.sp_multiresultset();
var Customers = 多结果集存储过程.GetResult<Customer>();
var Employees = 多结果集存储过程.GetResult<Employee>();
GridView1.DataSource = from emp
in Employees where emp.FirstName.Contains("A")
select emp;
GridView1.DataBind();
GridView2.DataSource = from c
in Customers where c.CustomerID.StartsWith("A")
select c;
GridView2.DataBind();

使用存储过程新增数据

存储过程除了可以直接调用之外,还可以用于实体的增删改操作。还记得在《一步一步学Linq to sql(三):增删改》中创建的留言簿程序吗?下面我们就来改造这个程序,使用存储过程而不是系统生成的SQL实现实体增删改。首先,我们创建下面的存储过程

create proc sendmessage
@username varchar(50),
@message varchar(500)
as
insert into tbguestbook
(id,username,posttime,[message],isreplied,reply)
values
(newid(),@username,getdate(),@message,0,'')

然后,打开留言簿dbml,把存储过程从服务器资源管理器拖拽到设计视图上。右键点击tbGuestBook实体类,选择配置行为。如下图,为插入操作选择刚才创建的存储过程方法,并进行参数匹配:

由于我们的存储过程只接受2个参数,相应修改以下创建留言的按钮处理事件:

protected
void btn_SendMessage_Click(object sender,
EventArgs e)
{
tbGuestBook gb =
new tbGuestBook();
gb.UserName = tb_UserName.Text;
gb.Message = tb_Message.Text;
ctx.tbGuestBooks.Add(gb);
ctx.SubmitChanges();
SetBind();
}

运行程序后可以发现,在提交修改的时候调用了下面的SQL:

EXEC @RETURN_VALUE = [dbo].[sendmessage] @username = @p0, @message = @p1
-- @p0: Input AnsiString (Size = 5; Prec = 0; Scale = 0) [zhuye]
-- @p1: Input AnsiString (Size = 11; Prec = 0; Scale = 0) [new message]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []

使用存储过程删除数据

创建如下存储过程:

create proc delmessage
@id uniqueidentifier
as
delete tbguestbook where id=@id

按照前面的步骤生成存储过程方法,并为删除操作执行这个存储过程方法。在选择参数的时候我们可以看到,ID分当前值和原始值,我们选择当前值即可,如下图:

无须改动任何逻辑代码,进行删除留言操作后可以跟踪到下面的SQL:

EXEC @RETURN_VALUE = [dbo].[delmessage] @id = @p0
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [9e3c5ee3-2575-458e-899d-4b0bf73e0849]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []

使用存储过程更改数据

创建如下存储过程:

create proc replymessage
@id uniqueidentifier,
@reply varchar(500)
as
update tbguestbook set reply=@reply,isreplied=1
where id=@id

由于更新的时候并不会更新主键,所以我们可以为两个参数都指定当前值。回复留言后可以跟踪到下面的SQL:

EXEC @RETURN_VALUE = [dbo].[replymessage] @id = @p0, @reply = @p1
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [67a69d0f-a88b-4b22-8939-fed021eb1cb5]
-- @p1: Input AnsiString (Size = 6; Prec = 0; Scale = 0) [464456]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []

假设有这样一种应用,我们需要修改留言簿中不合法的用户名:

create proc modiusername
@oldusername varchar(50),
@newusername varchar(50)
as
update tbguestbook set username=@newusername
where username = @oldusername

有个网友起名叫“admin”,我们要把所有这个名字修改为“notadmin”。那么,可以如下图设置update操作:

然后运行下面的测试代码:

var messages =
from gb in ctx.tbGuestBooks
select gb;
foreach (var gb
in messages)
{
if(gb.UserName ==
"admin")
gb.UserName = "notadmin";
}

运行程序后能跟踪到下面的SQL:

SELECT [t0].[ID], [t0].[UserName], [t0].[PostTime], [t0].[Message], [t0].[IsReplied], [t0].[Reply]
FROM [dbo].[tbGuestBook] AS [t0]
EXEC @RETURN_VALUE = [dbo].[modiusername] @oldusername = @p0, @newusername = @p1
-- @p0: Input AnsiString (Size = 5; Prec = 0; Scale = 0) [admin]
-- @p1: Input AnsiString (Size = 8; Prec = 0; Scale = 0) [notadmin]
-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: