在移动设备上有几种数据库可用,但是本书所用的都是SQL Server 2000 Windows CE Edition,是微软公司桌面数据库的精简版(本书以后简称为SQL Server CE)。这是为.NET Compact Framework选择的移动数据库,因为它完全支持.NET Compact Framework运行时环境和.NET Compact Framework开发环境。
大多数SQL Server CE程序有两个主要的数据库任务。
n 当断开时操作数据;
n 当设备连到桌面计算机时传送数据到SQL Server。
第一个数据库任务包含使用一般的ADO.NET类,这在前面已经讨论过了,至于提供者类,以后将讨论。在桌面系统上对于使用ADO.NET的程序员来说,.NET Compact Framework实现的ADO.NET的一般类都是很熟悉的。
第二个主要的数据库任务,SQL Server和SQL Server CE间的数据传输,不仅包括多个数据库引擎,也包括Microsoft Internet Information Services(IIS)。这是两种不同的机制,对合并复制和远程数据访问(RDA)可能有所帮助。
图11-1 显示在文件管
|
每个SQL Server数据库以单个文件储存在Windows CE设备上。推荐的文件扩展名为.sdf,文件名称为数据库的名称。如打开一个名为mydb.sdf的数据库连接,就要用全称mydb.sdf,不能用不带sdf后缀的mydb。因为每个数据库是一个文件,所以在文件管理器中可见。图11-1显示的是在文件管理器选择的一个数据库文件。
因为SQL Server CE数据库是Windows CE文件系统中的文件,所以它们能使用File和Directory类访问。例如,复制一个数据库文件、删除一个数据库文件或者测试一个指定名称的数据库文件是否存在。同样可以像其他文件那样进行约束。例如,如果文件当前未用,能删除一个数据库文件。
SQL Server CE能回收数据库文件里的空间。但它不能通过数据操作语言达到,也不能作为后台处理。为了回收SQL Server CE数据库的空间,必须使用SqlCeEngine类的Compact方法。
SQL Server CE编程比SQL Server编程简单,因为SQL Server CE支持的SQL语言是SQL Server的一个子集。因为SQL Server CE数据库是一个单用户数据库,它的安全通过一个文件密码控制,所以没必要有GRANT、DENY和PEVOKE语句。
SQL Server CE也缺少SQL Server的Transact SQL扩展:没有存储过程、语句批处理或DECLARE、SET、IF或WHILE语句。甚至一些标准的SQL也被去掉了,如不支持视图。不是所有的SQL Server数据类型都被包含,但是大多数缺少的数据类型都被转换成存在的一些类型。如Windows CE本身只支持Unicode,因此Unicode是惟一的字符数据类型。
SQL Server CE提供表、索引、默认值和参考完整性的支持。它也具有使用标准SQL数据操作语言(DML)在表中增加、修改和删除行的能力。因此,程序连接到数据库文件,提交INSERT、UPDATE和DELETE语句到数据库,能操作SQL Server CE的数据。读者可以自己写这些DML语句,或者用ADO.NET写它们,这是数据集同步到数据库的一个步骤。同样,SQL语句使用单引号描述字符串,当在代码引用SQL时,这是很方便的,因为避免了双引号的麻烦。
SQL Server CE程序可能比典型的SQL Server程序更基本,但是它应该提供了足够的功能去用于移动情况。Windows CE设备的可用内存是一个不可避免的限制,抛开这个尺寸限制,SQL Server CE为任何Windows CE程序提供足够的SQL命令集。
表11-1和表11-2分别列出了SQL Server CE支持和不支持的SQL Server的功能。
表11-1 SQL Server CE不支持的SQL Server功能
功 能 |
说 明 |
DCL(Data Control Language) |
在单用户数据库中不需要 |
DDL(Data Definition Language) |
这些大多数是对ANSI功能的SQL Server Transact SQL扩展,逻辑上,能位于SQL Server上。当运行在SQL Server CE时它必须编码到程序中 |
DML(Data Manipulation Language) |
这些大多数是对ANSI功能的SQL Server Transact SQL扩展 |
INFORMATION_SCHEMA TABLES |
这个替换成了MSysObjects 和MSysConstrains 表 |
表11-2 SQL Server CE支持的SQL Server功能
功 能 |
说 明 |
DDL |
只支持Unicode字符类型 |
DML |
|
函数 |
|
事务 |
事务分级别始终是READ COMMITTED,嵌套的限制是5,惟一的锁粒度是表级别,在事务期间有效。单向提交是惟一允许的提交类型 |
SQL Server CE语法比SQL Server烦杂些。有一个更小的footprint,意味着有更少的代码去“推断”想要的东西。如下面的SQL,在最后列定义和约束定义间缺少一个逗号,在SQL Server 2000中执行没有问题,但是在SQL Server CE中执行会产生一个语法错误。
CREATE TABLE Products
( ProductID integer not null primary key
, ProductName nchar(20) not null
, CategoryID integer not null
CONSTRAINT FKProductCategory foreign key (CategoryID)
references Categories(CategoryID)
)
图11-2 使用别名列名的
|
SELECT P.ProductID as ID
, P.ProductName as Name
, C.CategoryName as Category
FROM Products P
JOIN Categories C on C.CategoryID = P.CategoryID
对于指定一个DataGrid类的类名它并不是首选。使用DataGridColumnStyle对象更好些,因为它与底层的SELECT语句无关。
浏览和查询一个SQL Server CE数据库的主要工具是SQL Server CE Query Analyzer。像SQL Server 2000的Query Analyzer一样,SQL Server CE Query Analyzer提供了一个创建和提交特定查询的便利途径。
SQL Server CE Query Analyzer的安装依赖于安装SQL Server CE的开发环境。当安装SQL Server CE时,SQL Server CE Query Analyzer默认地不被安装到设备上。一旦Query Analyzer安装到设备上,才能直接从开始菜单或安装目录运行可执行文件Isqlw20.exe。单击文件管理器中的一个数据库文件也能打开Query Analyzer。
Query Analyzer允许查看数据库的结构信息,如图11-3所示,以及提交对那个数据库的查询,如图11-4所示。
图11-3 SQL Server CE Query 图11-4 SQL Server CE Query
Analyzer的Objects选项卡 Analyzer的SQL选项卡
Query Analyzer窗体显示一个最小化框,不是一个关闭框。如果使用Query Analyzer去查看数据库,然后在最小化框上单击,那么Query Analyzer将消失,但是没有关闭,也不关闭正在显示的数据库。任何试图访问该数据库的其他程序只能等到关闭与Query Analyzer连接的数据库(在Objects选项卡,在有数据库名称的树节点上单击,然后在工具条上单击Stop按钮)或关闭Query Analyzer(选择Tools Exit)才能访问。
访问SQL Server CE的第一个程序位于CreateDatabase项目,程序用于创建和组装一个数据库。
使用System.Data.SqlServerCe命名空间的Engine类创建一个SQL Server CE数据库。因为每个SQL Server CE数据库都是一个单个文件,要做的就是告诉Engine对象那个文件的路径和名称。也能在Engine类的构造函数或设置对象LocalConnectionString属性中告之路径和文件名。在任一情况中,必须在执行Engine类的CreateDatabase方法前指定文件名,因为这个方法不带任何参数。
创建SQL Server CE数据库的代码如图11-5所示。
图11-5 CreateDatabase程序
private string strFile = @"My Documents/ourProduceCo.sdf";
private string strConn = "Data Source=" +
@"My Documents/ourProduceCo.sdf";
private void mitemCreateDB_Click(object sender, EventArgs e)
{
if ( File.Exists(strFile) ) { File.Delete(strFile); }
SqlCeEngine dbEngine = new SqlCeEngine();
dbEngine.LocalConnectionString = strConn;
try
{
dbEngine.CreateDatabase();
}
catch( SqlCeException exSQL )
{
MessageBox.Show("Unable to create database at " +
strFile +
". Reason: " +
exSQL.Errors[0].Message );
}
}
虽然Engine类有一个CreateDatabase方法,但是它没有DropDatabase方法。删除一个数据库必须通过提交一个DROP DATABASE语句到SQL Server CE数据库或使用删除文件的File类来完成。
现在已经创建了SQL Server CE数据库,接下来就能用表、索引和数据组装它。读者可能认为,用Engine类完成这个操作,但Engine只操作一个完整的数据库,不操作数据库内的单独组件,如表。使用标准的SQL DDL语句,例如CREATE TABLE和CREATE INDEX,以及标准的SQL DML语句INSERT、UPDATE和DELETE组装SQL Server CE数据库。
提交SQL语句到数据库需要两个类,一个打开连接,一个提交语句,分别是SqlCeConnection和SqlCeCommand类。
SqlCeConnection类打开一个数据库的连接,这需要数据库文件的名称。SqlCeCommand类使用Execute方法提交一次一个SQL语句到数据库,该方法需要使用连接对象和要提交的SQL语句。SqlCeConnection和DML语句是SqlCeCommand对象的属性。在命令的Execute方法调用之前必须设置它们,类连接必须打开。表11-3描述了SQLCeComnection类的执行方法。
表11-3 SqlCeCommand类的执行方法
方 法 |
功 能 |
ExecuteNonQuery |
执行一个SQL语句,不返回行,例如INSERT或CREATE |
ExecuteScalar |
执行一个SQL语句,只返回一个值,例如SELECT SUM(Value) FROM Orders WHERE CustomerID = "ABCD" |
ExecuteReader |
执行一个SQL语句,返回多列或多行 |
下面显示的创建两个简单表的代码:
private void mitemCreateTables_Click(object sender, EventArgs e)
{
SqlCeConnection connDB = new SqlCeConnection();
SqlCeCommand cmndDB = new SqlCeCommand();
connDB.ConnectionString = strConn;
connDB.Open();
cmndDB.Connection = connDB;
cmndDB.CommandText =
" CREATE TABLE Categories " +
" ( CategoryID integer not null " +
" CONSTRAINT PKCategories PRIMARY KEY " +
" , CategoryName nchar(20) not null " +
" )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" CREATE TABLE Products " +
" ( ProductID integer not null " +
" CONSTRAINT PKProducts PRIMARY KEY " +
" , ProductName nchar(20) not null " +
" , CategoryID integer not null " +
" , CONSTRAINT FKProdCat " +
" foreign key (CategoryID) " +
" references Categories(CategoryID) " +
" )";
cmndDB.ExecuteNonQuery();
connDB.Close();
}
SQL Server CE为代码指定的3个约束自动地创建索引,只需要为不包含约束的索引提交CREATE INDEX语句就可以了。
下面显示的是插入行到表的代码:
private void mitemLoadData_Click(object sender, EventArgs e)
{
SqlCeConnection connDB = new SqlCeConnection();
SqlCeCommand cmndDB = new SqlCeCommand();
connDB.ConnectionString = strConn;
connDB.Open();
cmndDB.Connection = connDB;
cmndDB.CommandText =
" INSERT Categories " +
" (CategoryID, CategoryName)" +
" VALUES (1, 'Franistans' )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Categories " +
" (CategoryID, CategoryName)" +
" VALUES (2, 'Widgets' )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (11, 'Franistans - Large', 1 )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (12, 'Franistans - Medium', 1 )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (13, 'Franistans - Small', 1 )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (21, 'Widgets - Large', 2 )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (22, 'Widgets - Medium', 2 )";
cmndDB.ExecuteNonQuery();
cmndDB.CommandText =
" INSERT Products " +
" (ProductID, ProductName, CategoryID)" +
" VALUES (23, 'Widgets - Small', 2 )";
cmndDB.ExecuteNonQuery();
connDB.Close();
}
因为代码的SQL语句没有数据返回,所以使用ExecuteNonQuery方法。
这样,两个类、很少的方法和属性、一些标准SQL,就组装了一个数据库。这是一个简单的数据库,但是接下来将使用同样的类、属性和方法产生一个更复杂的数据库。
现在已经用一些数据组装了SQL Server CE数据库,接下来将考虑如何显示数据给用户。下面由两层程序开始,使用SqlCeDataReader类,然后移到3层程序。
SqlCeDataReader类允许程序访问一个SQL查询返回的行。它一次只提供对一行只读的向前访问。一旦定位到一行,使用DataReader类的方法去访问位于那行的各个字段的信息。这些方法中许多函数的名字里都以Get开头,字段是0开始的数组。这样,如果底层SELECT语句是SELECT ProductID,ProductName FROM Products,那么接收来自DataReader对象当前行的ProductID字段(字段0)的代码如下:
intVar = drdrDataReader.GetInt32(0);
// 或明显低效的
intVar = drdrDataReader.GetValue(0);
// 或
intVar = drdrDataReader.Item["ProductID"];
同样,接收ProductName 字段将是下列代码:
strVar = drdrDataReader.GetString(1);
// 或
strVar = drdrDataReader.GetValue(1);
// 或
strVar = drdrDataReader.Item["ProductName"];
不能直接创建一个DataReader对象,必须使用SqlCeCommand对象的ExecuteReader方法去创建DataReader对象。当第一次创建时,DataReader被打开,但是仍没有定位到一行,有两种方法定位到一行。
第一种使用DataReader对象的Read方法去移动行。第二种是使用DataReader对象的Seek方法去定位到指定的行。
n 底层数据表是有索引的。
n 在执行ExecuteReader方法前设置了SqlCeCommand对象的额外属性。
n 调用Seek方法指定索引名称和关键字值。
n 调用Read方法之后跟着调用Seek方法。
图11-6 DataReader程序
|
下面例子是为了示例Read和Seek方法,如图11-6所示。ComboBox控件装载产品关键值。当用户单击ComboBox控件,选择产品的字段显示在TextBox控件中。
程序代码如下。
using System;
using System.IO;
using System.Drawing;
using System.Collections;
using System.Windows.Forms;
using System.Data;
using System.Data.Common;
using System.Data.SqlServerCe;
namespace DataReader
{
///
/// Summary description for FormMain
///
public class FormMain : System.Windows.Forms.Form
{
internal System.Windows.Forms.Label lblCategoryName;
internal System.Windows.Forms.Label lblProductName;
internal System.Windows.Forms.Label lblProductID;
internal System.Windows.Forms.TextBox textCategoryName;
internal System.Windows.Forms.TextBox textProductName;
internal System.Windows.Forms.TextBox textProductID;
internal System.Windows.Forms.ComboBox comboKeys;
public FormMain()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
}
///
/// Clean up any resources being used.
///
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
#region Windows Form Designer generated code
///
/// The main entry point for the application
///
static void Main()
{
Application.Run(new FormMain());
}
// 文件路径和名称
private string strFile = @"My Documents/ourProduceCo.sdf";
// 连接字符串
private string strConn = "Data Source=" +
@"My Documents/ourProduceCo.sdf";
// 选择产品主键
private string strGetProductIDs =
" SELECT ProductID " + " FROM Products ";
// 选择一个产品,与分类关联
private string strGetOneProduct =
" SELECT ProductID, ProductName, CategoryName " +
" FROM Products P " +
" JOIN Categories C on C.CategoryID = P.CategoryID " +
" WHERE P.ProductID = ";
// ComboBox装载时使用SelectIndexChanged 事件
private bool boolLoading = true;
private void FormMain_Load(object sender, EventArgs e)
{
// 显示关闭框
this.MinimizeBox = false;
Application.DoEvents();
// 确保数据库存在
if (! File.Exists(strFile) )
{
MessageBox.Show(
"Database not found. Run the CreateDatabase " +
"program for this chapter first. Then run " +
"this program.");
}
// 装载产品主键到ComboBox
// 并选择第一个
LoadProductIDs();
comboKeys.SelectedIndex = 0;
}
private void comboKeys_SelectedIndexChanged(object sender,
EventArgs e)
{
// 已经选择一个产品主键;接收并显示相关的产品
if (! boolLoading )
{
LoadOneProduct((int)comboKeys.SelectedItem);
}
}
private void textProductName_Validated(object sender,
EventArgs e)
{
// 在数据库中更新这个产品行
UpdateSelectedRow(int.Parse(textProductID.Text),
textProductName.Text);
}
private void LoadProductIDs()
{
// 清空ComboBox.
comboKeys.Items.Clear();
// A connection, a command, and a reader
SqlCeConnection connDB =
new SqlCeConnection(strConn);
SqlCeCommand cmndDB =
new SqlCeCommand(strGetProductIDs, connDB);
SqlCeDataReader drdrDB;
// 打开连接
connDB.Open();
// 提交SQL语句并接收SqlCeReader作为结果集
drdrDB = cmndDB.ExecuteReader();
// 读每行。只添加列内容作为ComboBox的条目
// 完成时关闭reader
while ( drdrDB.Read() )
{
comboKeys.Items.Add(drdrDB["ProductID"]);
}
drdrDB.Close();
// 关闭连接
connDB.Close();
// 开始响应ComboBox的SelectedIndexChanged事件
this.boolLoading = false;
}
private void LoadOneProduct( int intProductID)
{
// A connection, a command, and a reader
SqlCeConnection connDB = new SqlCeConnection(strConn);
SqlCommand cmndDB = new SqlCommand(strSQL,connDB);
SqlCeDataReader drdrDB;
// 打开连接
connDB.Open();
// 设置Command对象去接收通过索引来自一个表的行
// 然后接收reader.
cmndDB.Connection = connDB;
cmndDB.CommandType = CommandType.TableDirect;
cmndDB.CommandText = "Products";
cmndDB.IndexName = "PKProducts";
drdrDB = cmndDB.ExecuteReader();
// 只接收Products 表中具有ComboBox选择的ProductID的第一行
// 装载字段到窗体的控件
// 关闭reader.
drdrDB.Seek(DbSeekOptions.FirstEqual, intProductID);
if( drdrDB.Read() )
{
LoadControlsFromRow(drdrDB);
}
drdrDB.Close();
// Close the connection.
connDB.Close();
}
private void LoadControlsFromRow( SqlCeDataReader drdrDB)
{
// Transfer the column titles and the field
// contents of the current row from the
// reader to the form's controls.
lblProductID.Text = drdrDB.GetName(0);
textProductID.Text = drdrDB.GetValue(0).ToString();
lblProductName.Text = drdrDB.GetName(1);
textProductName.Text = drdrDB.GetValue(1).ToString();
lblCategoryName.Text = drdrDB.GetName(2);
textCategoryName.Text = drdrDB.GetValue(2).ToString();
}
private void UpdateSelectedRow(int intProductID,
string strProductName)
{
// A connection and a command
SqlCeConnection connDB = new SqlCeConnection(strConn);
SqlCeCommand cmndDB = new SqlCeCommand();
// 打开连接
connDB.Open();
// 为选择的产品更新产品名称
cmndDB.Connection = connDB;
cmndDB.CommandText =
" UPDATE Products " +
" SET ProductName = " + "'" + strProductName + "'" +
" WHERE ProductID = " + intProductID;
cmndDB.ExecuteNonQuery();
// 关闭连接
connDB.Close();
}
}
}
LoadProductIDs函数提交一个SELECT语句,使用一个DataReader对象去处理SELECT语句返回的所有行。注意,SqlCeCommand对象的所有属性必须在DataReader对象创建前初始化。因为当创建时不定位到第一行,必须在迭代循环开始时调用Read方法,而不是循环结束。这有助于更清晰的代码,是对原来的ADO.NET类的一个改善。
图11-7 使用另一个LoadOne
|
如图11-7所示的输出。所有列来自Products表,使用的是一个内建的Seek方法。下面代码显示的是LoadOneProduct方法的另一个版本。它使用WHERE子句而不是Seek方法索引去指定想要的产品。输出如图11-7所示,这个版本的LoadOneProduct方法能从多个表中获得字段,较多的是CategoryName字段与每个产品关联,较少的是CategoryID字段关联。然而,前面也提到了,这个版本的方法执行比较慢。
private void LoadOneProduct( int intProductID)
{
// 追加想要ProductID到SELECT语句
string strSQL = strGetOneProduct + intProductID;
// A connection, a command, and a reader
SqlCeConnection connDB = new SqlCeConnection(strConn);
SqlCommand cmndDB = new SqlCommand(strSQL,connDB);
SqlCeDataReader drdrDB;
// 打开连接
connDB.Open();
// 提交SQL语句,接收SqlCeReader 作为单行结果集
drdrDB = cmndDB.ExecuteReader();
// 只读第一行。显示它。关闭reader.
if ( drdrDB.Read() )
{
LoadControlsFromRow(drdrDB);
}
drdrDB.Close();
// 关闭连接
connDB.Close();
}
前面提到,从ExecuteReader方法返回的DataReader对象不定位到第一行。但是它已经被打开,必须关闭它。不像桌面的位于多用户环境的DataReader对象,SqlCeDataReader对象是为移动设备而写的。这意味着在同一连接上一次能有多个reader。这是一个必需的性能,允许每个数据库一次只有一个打开的SqlCeConnection对象。
DataReader对象的当前行和DataRow对象不是一样的。DataRow是一个到自身的对象,DataReader的当前行不是。如前面所述,使用DataReader本身的方法和属性访问DataReader对象的当前行的内容。
现在,已经将数据显示给用户,接下来将继续允许用户修改数据并且提交这些修改数据到数据库。当第一次组装数据库时,已经给出了一个例子。下面是已经介绍过的类似代码例子。
使用textProductName.Validated事件允许用户改变产品名称的程序,代码如下:
private void textProductName_Validated(object sender, EventArgs e)
{
// 在数据库中更新这个产品行
UpdateSelectedRow(int.Parse(textProductID.Text), textProductName.Text);
}
下面代码显示的是更新产品行的调用函数。
private void UpdateSelectedRow(int intProductID,
string strProductName)
{
// A connection and a command
SqlCeConnection connDB = new SqlCeConnection(strConn);
SqlCeCommand cmndDB = new SqlCeCommand();
// 打开连接
connDB.Open();
// 更新选择的产品的名称
cmndDB.Connection = connDB;
cmndDB.CommandText =
" UPDATE Products " +
" SET ProductName = " + "'" + strProductName + "'" +
" WHERE ProductID = " + intProductID;
cmndDB.ExecuteNonQuery();
// 关闭连接
connDB.Close();
}
如果程序允许多个字段被更新,应该在每个字段的Validated事件处理中设置一个“需要更新”的标志,如必要可反馈到ComboBox控件的SelectedIndexChanged和窗体的Close事件上。
如果这个程序只是访问一个数据库的程序,不必每次提交一个查询到数据库就打开和关闭连接。在这种情况,将Open和Close调用移到窗体的Load和Closed事件中。
通过学习已经知道,如何使用SqlCeConnection、SqlCeCommand和SqlCeDataReader类接收来自一个SQL Server CE数据库的数据,并在窗体的控件上表现数据给用户,从而用户接收改变,提交这些改变给数据库。并且,这章也讲了两层程序中SQL Server CE的介绍。