ADO.NET
关系型数据库管理系统(Relational database management systems,RDBMSs)是数据存储最普遍的形式。有了 ADO.NET,System.Data 和相关的命名空间,访问关系型数据更容易。这一节,我们将学习多种方法在F# 中使用 ADO.NET。
注意
所有的数据库提供程序都使用连接字符串指定数据库连接。在http://www.connectionstrings.com 上有关于连接字符串的汇总。
这一节的所有例子都使用 SQL Server 2005 Express Edition 和 AdventureWorks 数据库,两个都可以从http://www.microsoft.com *下载;把这些例子导出到其他关系型数据库也是很容易的。要在 SQL Server 2005 Express Edition 中使用这个数据库,需要下面的连接设置,或者根据你的系统作相应调整:
<connectionStrings>
<add
name="MyConnection"
connectionString="
Database=AdventureWorks;
Server=.\SQLExpress;
Integrated Security=SSPI;
AttachDbFilename=
C:\ProgramFiles\Microsoft SQL Server\MSSQL.1\MSSQL\Data\AdventureWorks_Data.mdf"
providerName="System.Data.SqlClient" />
</connectionStrings>
[
上面的配置文件缺少根结点:<configuration></configuration>
用下面的连接字符串也可以:
<configuration>
<connectionStrings>
<add
name="MyConnection"
connectionString="
DataSource=.\sqlexpress;
InitialCatalog=AdventureWorks;
IntegratedSecurity=True;
Pooling=False"
providerName=".NET Framework Data Providerfor SQL Server" />
</connectionStrings>
</configuration>
]
我们将在“ADO.NET 扩展”一节讨论访问其他关系型数据库的选项。下面的例子演示了访问数据库的一种简单方法:
open System.Configuration
open System.Data
open System.Data.SqlClient
// get the connection string
let connectionString =
letconnectionSetting =
ConfigurationManager.ConnectionStrings.["MyConnection"]
connectionSetting.ConnectionString
let main() =
//create a connection
useconnection = new SqlConnection(connectionString)
//create a command
letcommand =
connection.CreateCommand(CommandText= "select * from Person.Contact",
CommandType =CommandType.Text)
//open the connection
connection.Open()
//open a reader to read data from the DB
usereader = command.ExecuteReader()
//fetch the ordinals
lettitle = reader.GetOrdinal("Title")
letfirstName = reader.GetOrdinal("FirstName")
letlastName = reader.GetOrdinal("LastName")
//function to read strings from the data reader
letgetString (r: #IDataReader) x =
if r.IsDBNull(x) then ""
else r.GetString(x)
//read all the items
while reader.Read() do
printfn "%s %s %s"
(getString reader title )
(getString reader firstName)
(getString reader lastName)
main()
前面代码的运行结果如下:
Mr. Gustavo Achong
Ms. Catherine Abel
Ms. Kim Abercrombie
Sr. Humberto Acevedo
Sra. Pilar Ackerman
Ms. Frances Adams
Ms. Margaret Smith
...
在前面的例子中,首先看到的是将要用到的连接字符串;之后,就创建连接:
using (new SqlConnection(connectionString))
注意,我们使用 use 关键字代替 let,这是保证在完成所做工作之后,能够关闭连接;use 关键字能够保证当连接超出其作用域后,会调用Dispose 方法。然后,使用连接创建 SqlCommand 类,再设置它的 CommandText 属性,指定需要执行的命令:
let command =
connection.CreateCommand(CommandText = "select * fromPerson.Contact",
CommandType =CommandType.Text)
接下来,执行这个命令创建 SqlDataReader 类,用这个类读数据库:
use reader = command.ExecuteReader()
这个命令也使用 use 绑定,代替 let,确保可以正确关闭。
如果必须为每个查询写大量的代码,那么,在 F# 中可能并不需要写数据访问的代码。有一个简单的方法,创建执行命令的库函数,这样。就能把使用的连接、运行的命令参数化。
下面的例子演示如何写出这样一个函数,实现 execCommand 函数,使用 Seq.generate,它能产生可枚举的序列集合。generate 函数有三个参数:第一个函数打开到数据库的连接,在每次枚举结果集合时调用,这个函数被称为开启者(opener),也能够用它来打开对文件的连接;第二个函数生成集合中的元素,称为生产者(generator)。这段代码用一行数据创建了字典(Dictionary)对象;第三个函数是用于关闭数据库或读取文件的连接:
open System.Configuration
open System.Collections.Generic
open System.Data
open System.Data.SqlClient
open System.Data.Common
open System
/// create and open an SqlConnection objectusing the connection string found
/// in the configuration file for the givenconnection name
let openSQLConnection(connName:string) =
letconnSetting = ConfigurationManager.ConnectionStrings.[connName]
letconn = new SqlConnection(connSetting.ConnectionString)
conn.Open()
conn
/// create and execute a read command for aconnection using
/// the connection string found in theconfiguration file
/// for the given connection name
let openConnectionReader connName cmdString=
letconn = openSQLConnection(connName)
letcmd = conn.CreateCommand(CommandText=cmdString,
CommandType =CommandType.Text)
letreader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
reader
/// read a row from the data reader
let readOneRow (reader: #DbDataReader) =
ifreader.Read() then
letdict = new Dictionary<string, obj>()
forx in [ 0 .. (reader.FieldCount - 1) ] do
dict.Add(reader.GetName(x), reader.[x])
Some(dict)
else
None
/// execute a command using theSeq.generate
let execCommand (connName: string)(cmdString: string) =
Seq.generate
//This function gets called to open a connection and create a reader
(fun () -> openConnectionReader connNamecmdString)
//This function gets called to read a single item in
//the enumerable for a reader/connection pair
(fun reader -> readOneRow(reader))
(fun reader -> reader.Dispose())
[
以下的两段出现在这里,重复。
已经在后面单独出现了。
]
/// open the contacts table
let contactsTable =
execCommand
"MyConnection"
"select* from Person.Contact"
/// print out the data retrieved from thedatabase
for row in contactsTable do
forcol in row.Keys do
printfn"%s = %O" col (row.Item(col))
[
除 System.Configuration、System.Data 以外,还需要 Fsharp.PowerPack
]
定义了execCommand 函数之后,再访问数据库就变得非常容易了。调用 execCommand,把需要的连接和命令作为参数传入,然后,枚举结果。就如下面的示例:
let contactsTable =
execCommand
"MyConnection"
"select * from Person.Contact"
for row in contactsTable do
for col in row.Keys do
printfn "%s = %O" col(row.Item(col))
运行结果如下:
...
ContactID = 18
NameStyle = False
Title = Ms.
FirstName = Anna
MiddleName = A.
LastName = Albright
Suffix =
EmailAddress = anna0@adventure-works.com
EmailPromotion = 1
Phone = 197-555-0143
PasswordHash =6Hwr3vf9bo8CYMDbLuUt78TXCr182Vf8Zf0+uil0ANw=
PasswordSalt = SPfSr+w=
AdditionalContactInfo =
rowguid =b6e43a72-8f5f-4525-b4c0-ee84d764e86f
ModifiedDate = 01/07/2002 00:00:00
...
有一件事情必须注意,处理关系型数据库时,需要保证连接及时关闭。很快地关闭连接,可以使连接用于其他数据库用户,提高并行访问能力。我们看一下前面的例子,看它是如何创建连接,并自动清除的。在前面的例子中,开启者在每次使用Seq.iter 枚举集合时调用函数 openConnectionReader;它使用可枚举对象遍历数据,重复使用生产者函数产生单独的结果。每次调用 Seq.iter 就创建一个SqlConnection [ 不应该是SqlDataReader ]和 SqlDataReader 对象。在遍历结束时,或者由于某些原因遍历突然终止时,这些对象必须关闭。幸运的是,Seq.generate 函数能够在遍历完成或部分完成时,清除资源。当客户端完成枚举集合时,就调用关闭函数。应该使用这个函数在SqlDataReader上调用 IDisposable.Dispose 方法,触发关闭。还必须关闭对应的 SqlConnection 对象,它是由关闭 SqlDataReader、再关闭数据库连接而完成的。
command.ExecuteReader(CommandBehavior.CloseConnection)
为不使连接打开时间太长,应该在遍历可枚举集合的结果期间,避免复杂或过度耗时的操作,特别要避免用户与集合因某种原因的交互。例如,下面的代码重写了前面的例子,用户通过按回车键移动到下一条记录,这对数据库的性能是有害的:
for row in contactsTable do
for col in row.Keys do
printfn "%s = %O" col(row.Item(col))
printfn "Press <enter> to see nextrecord"
read_line() |> ignore
如果你想多次使用集合,或者想让用户交互,通常应该把它转换成列表或数组,就像下面的代码:
let contactsTable =
execCommand
"select * from Person.Contact"
"MyConnection"
let contactsList = Seq.toList contactsTable
虽然当游标被垃圾回收时,连接将被关闭,但这个过程通常要花很长时间,特别是系统在高压力下。例如,如果写的代码运行服务器应用程序,处理大量并发用户,那么,不关闭连接将引起错误,因为服务器会很快用完数据库连接。